Skip to content

Commit b07e25a

Browse files
committed
feat: add PriceFeedCompressor
1 parent 52106f2 commit b07e25a

12 files changed

+716
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// SPDX-License-Identifier: MIT
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2024.
4+
pragma solidity ^0.8.17;
5+
6+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
7+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
8+
9+
import {AddressIsNotContractException} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol";
10+
import {IPriceOracleV3, PriceFeedParams} from "@gearbox-protocol/core-v3/contracts/interfaces/IPriceOracleV3.sol";
11+
import {IPriceFeed, IUpdatablePriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol";
12+
import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol";
13+
import {PriceFeedType} from "@gearbox-protocol/sdk-gov/contracts/PriceFeedType.sol";
14+
15+
import {IStateSerializer} from "../interfaces/IStateSerializer.sol";
16+
import {IStateSerializerTrait} from "../interfaces/IStateSerializerTrait.sol";
17+
import {NestedPriceFeeds} from "../libraries/NestedPriceFeeds.sol";
18+
import {PriceFeedAnswer, PriceFeedMapEntry, PriceFeedTreeNode} from "./Types.sol";
19+
20+
interface ImplementsPriceFeedType {
21+
/// @dev Annotates `priceFeedType` as `uint8` instead of `PriceFeedType` enum to support future types
22+
function priceFeedType() external view returns (uint8);
23+
}
24+
25+
/// @title Price feed compressor
26+
/// @notice Allows to fetch all useful data from price oracle in a single call
27+
/// @dev The contract is not gas optimized and is thus not recommended for on-chain use
28+
contract PriceFeedCompressor is IVersion, Ownable {
29+
using Address for address;
30+
using NestedPriceFeeds for IPriceFeed;
31+
32+
/// @notice Contract version
33+
uint256 public constant override version = 3_10;
34+
35+
/// @notice Map of state serializers for different price feed types
36+
/// @dev Serializers only apply to feeds that don't implement `IStateSerializerTrait` themselves
37+
mapping(uint8 => address) public serializers;
38+
39+
/// @notice Emitted when new state serializer is set for a given price feed type
40+
event SetSerializer(uint8 indexed priceFeedType, address indexed serializer);
41+
42+
/// @notice Constructor
43+
/// @param owner Contract owner
44+
constructor(address owner) {
45+
transferOwnership(owner);
46+
}
47+
48+
/// @notice Sets state serializer for a given price feed type (unsets if `serializer` is `address(0)`)
49+
function setSerializer(uint8 priceFeedType, address serializer) external onlyOwner {
50+
if (serializer != address(0) && !serializer.isContract()) revert AddressIsNotContractException(serializer);
51+
if (serializers[priceFeedType] != serializer) {
52+
serializers[priceFeedType] = serializer;
53+
emit SetSerializer(priceFeedType, serializer);
54+
}
55+
}
56+
57+
/// @notice Returns all potentially useful price feeds data for a given price oracle in the form of two arrays:
58+
/// - `priceFeedMap` is a set of entries in the map (token, reserve) => (priceFeed, stalenessPeirod).
59+
/// These are all the price feeds one can actually query via the price oracle.
60+
/// - `priceFeedTree` is a set of nodes in a tree-like structure that contains detailed info of both feeds
61+
/// from `priceFeedMap` and their underlying feeds, in case former are nested, which can help to determine
62+
/// what underlying feeds should be updated to query the nested one.
63+
/// @dev `priceFeedTree` can have duplicate entries since a price feed can both be in `priceFeedMap` for one or
64+
/// more (token, reserve) pairs, and serve as an underlying feed in one or more nested feeds.
65+
/// If there are two identical nodes in the tree, then subtrees of these nodes are also identical.
66+
function getPriceFeeds(address priceOracle)
67+
external
68+
view
69+
returns (PriceFeedMapEntry[] memory priceFeedMap, PriceFeedTreeNode[] memory priceFeedTree)
70+
{
71+
address[] memory tokens = IPriceOracleV3(priceOracle).getTokens();
72+
uint256 numTokens = tokens.length;
73+
74+
priceFeedMap = new PriceFeedMapEntry[](2 * numTokens);
75+
uint256 priceFeedMapSize;
76+
uint256 priceFeedTreeSize;
77+
78+
for (uint256 i; i < 2 * numTokens; ++i) {
79+
address token = tokens[i % numTokens];
80+
bool reserve = i >= numTokens;
81+
82+
PriceFeedParams memory params = reserve
83+
? IPriceOracleV3(priceOracle).reservePriceFeedParams(token)
84+
: IPriceOracleV3(priceOracle).priceFeedParams(token);
85+
86+
// can only happen to reserve price feeds
87+
if (params.priceFeed == address(0)) continue;
88+
89+
priceFeedMap[priceFeedMapSize++] = PriceFeedMapEntry({
90+
token: token,
91+
reserve: reserve,
92+
priceFeed: params.priceFeed,
93+
stalenessPeriod: params.stalenessPeriod
94+
});
95+
priceFeedTreeSize += _getPriceFeedTreeSize(params.priceFeed);
96+
}
97+
assembly {
98+
mstore(priceFeedMap, priceFeedMapSize)
99+
}
100+
101+
priceFeedTree = new PriceFeedTreeNode[](priceFeedTreeSize);
102+
uint256 offset;
103+
for (uint256 i; i < priceFeedMapSize; ++i) {
104+
offset = _loadPriceFeedTree(priceFeedMap[i].priceFeed, priceFeedTree, offset);
105+
}
106+
}
107+
108+
/// @dev Computes the size of the `priceFeed`'s subtree (recursively)
109+
function _getPriceFeedTreeSize(address priceFeed) internal view returns (uint256 size) {
110+
size = 1;
111+
(address[] memory underlyingFeeds,) = IPriceFeed(priceFeed).getUnderlyingFeeds();
112+
for (uint256 i; i < underlyingFeeds.length; ++i) {
113+
size += _getPriceFeedTreeSize(underlyingFeeds[i]);
114+
}
115+
}
116+
117+
/// @dev Loads `priceFeed`'s subtree (recursively)
118+
function _loadPriceFeedTree(address priceFeed, PriceFeedTreeNode[] memory priceFeedTree, uint256 offset)
119+
internal
120+
view
121+
returns (uint256)
122+
{
123+
PriceFeedTreeNode memory node = _getPriceFeedTreeNode(priceFeed);
124+
priceFeedTree[offset++] = node;
125+
for (uint256 i; i < node.underlyingFeeds.length; ++i) {
126+
offset = _loadPriceFeedTree(node.underlyingFeeds[i], priceFeedTree, offset);
127+
}
128+
return offset;
129+
}
130+
131+
/// @dev Returns price feed tree node, see `PriceFeedTreeNode` for detailed description of struct fields
132+
function _getPriceFeedTreeNode(address priceFeed) internal view returns (PriceFeedTreeNode memory data) {
133+
data.priceFeed = priceFeed;
134+
data.decimals = IPriceFeed(priceFeed).decimals();
135+
136+
try ImplementsPriceFeedType(priceFeed).priceFeedType() returns (uint8 priceFeedType) {
137+
data.priceFeedType = priceFeedType;
138+
} catch {
139+
data.priceFeedType = uint8(PriceFeedType.CHAINLINK_ORACLE);
140+
}
141+
142+
try IPriceFeed(priceFeed).skipPriceCheck() returns (bool skipCheck) {
143+
data.skipCheck = skipCheck;
144+
} catch {}
145+
146+
try IUpdatablePriceFeed(priceFeed).updatable() returns (bool updatable) {
147+
data.updatable = updatable;
148+
} catch {}
149+
150+
try IStateSerializerTrait(priceFeed).serialize() returns (bytes memory specificParams) {
151+
data.specificParams = specificParams;
152+
} catch {
153+
address serializer = serializers[data.priceFeedType];
154+
if (serializer != address(0)) {
155+
data.specificParams = IStateSerializer(serializer).serialize(priceFeed);
156+
}
157+
}
158+
159+
(data.underlyingFeeds, data.underlyingStalenessPeriods) = IPriceFeed(priceFeed).getUnderlyingFeeds();
160+
161+
try IPriceFeed(priceFeed).latestRoundData() returns (uint80, int256 price, uint256, uint256 updatedAt, uint80) {
162+
data.answer = PriceFeedAnswer({price: price, updatedAt: updatedAt, success: true});
163+
} catch {}
164+
}
165+
}

contracts/compressors/Types.sol

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-License-Identifier: MIT
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2024.
4+
pragma solidity ^0.8.17;
5+
6+
/// @notice Price feed answer packed in a struct
7+
struct PriceFeedAnswer {
8+
int256 price;
9+
uint256 updatedAt;
10+
bool success;
11+
}
12+
13+
/// @notice Represents an entry in the price feed map of a price oracle
14+
struct PriceFeedMapEntry {
15+
address token;
16+
bool reserve;
17+
address priceFeed;
18+
uint32 stalenessPeriod;
19+
}
20+
21+
/// @notice Represents a node in the price feed "tree"
22+
/// @param priceFeed Price feed address
23+
/// @param decimals Price feed's decimals (might not be equal to 8 for lower-level)
24+
/// @param priceFeedType Price feed type (same as `PriceFeedType` but annotated as `uint8` to support future types),
25+
/// defaults to `PriceFeedType.CHAINLINK_ORACLE`
26+
/// @param skipCheck Whether price feed implements its own staleness and sanity check, defaults to `false`
27+
/// @param updatable Whether it is an on-demand updatable (aka pull) price feed, defaults to `false`
28+
/// @param specificParams ABI-encoded params specific to this price feed type, filled if price feed implements
29+
/// `IStateSerializerTrait` or there is a state serializer set for this type
30+
/// @param underlyingFeeds Array of underlying feeds, filled when `priceFeed` is nested
31+
/// @param underlyingStalenessPeriods Staleness periods of underlying feeds, filled when `priceFeed` is nested
32+
/// @param answer Price feed answer packed in a struct
33+
struct PriceFeedTreeNode {
34+
address priceFeed;
35+
uint8 decimals;
36+
uint8 priceFeedType;
37+
bool skipCheck;
38+
bool updatable;
39+
bytes specificParams;
40+
address[] underlyingFeeds;
41+
uint32[] underlyingStalenessPeriods;
42+
PriceFeedAnswer answer;
43+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// SPDX-License-Identifier: MIT
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2024.
4+
pragma solidity ^0.8.17;
5+
6+
/// @title State serializer
7+
/// @notice Generic interface of a contract that is able to serialize state of other contracts
8+
interface IStateSerializer {
9+
function serialize(address) external view returns (bytes memory);
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// SPDX-License-Identifier: MIT
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2024.
4+
pragma solidity ^0.8.17;
5+
6+
/// @title State serializer trait
7+
/// @notice Generic interface of a contract that is able to serialize its own state
8+
interface IStateSerializerTrait {
9+
function serialize() external view returns (bytes memory);
10+
}
+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// SPDX-License-Identifier: MIT
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2024.
4+
pragma solidity ^0.8.17;
5+
6+
import {IPriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol";
7+
8+
uint256 constant MAX_UNDERLYING_PRICE_FEEDS = 8;
9+
10+
interface NestedPriceFeedWithSingleUnderlying is IPriceFeed {
11+
function priceFeed() external view returns (address);
12+
function stalenessPeriod() external view returns (uint32);
13+
}
14+
15+
interface NestedPriceFeedWithMultipleUnderlyings is IPriceFeed {
16+
function priceFeed0() external view returns (address);
17+
function priceFeed1() external view returns (address);
18+
function priceFeed2() external view returns (address);
19+
function priceFeed3() external view returns (address);
20+
function priceFeed4() external view returns (address);
21+
function priceFeed5() external view returns (address);
22+
function priceFeed6() external view returns (address);
23+
function priceFeed7() external view returns (address);
24+
25+
function stalenessPeriod0() external view returns (uint32);
26+
function stalenessPeriod1() external view returns (uint32);
27+
function stalenessPeriod2() external view returns (uint32);
28+
function stalenessPeriod3() external view returns (uint32);
29+
function stalenessPeriod4() external view returns (uint32);
30+
function stalenessPeriod5() external view returns (uint32);
31+
function stalenessPeriod6() external view returns (uint32);
32+
function stalenessPeriod7() external view returns (uint32);
33+
}
34+
35+
library NestedPriceFeeds {
36+
enum NestingType {
37+
NO_NESTING,
38+
SINGLE_UNDERLYING,
39+
MULTIPLE_UNDERLYING
40+
}
41+
42+
function getUnderlyingFeeds(IPriceFeed priceFeed)
43+
internal
44+
view
45+
returns (address[] memory feeds, uint32[] memory stalenessPeriods)
46+
{
47+
NestingType nestingType = getNestingType(priceFeed);
48+
if (nestingType == NestingType.SINGLE_UNDERLYING) {
49+
(feeds, stalenessPeriods) = getSingleUnderlyingFeed(NestedPriceFeedWithSingleUnderlying(address(priceFeed)));
50+
} else if (nestingType == NestingType.MULTIPLE_UNDERLYING) {
51+
(feeds, stalenessPeriods) =
52+
getMultipleUnderlyingFeeds(NestedPriceFeedWithMultipleUnderlyings(address(priceFeed)));
53+
}
54+
}
55+
56+
function getNestingType(IPriceFeed priceFeed) internal view returns (NestingType) {
57+
try NestedPriceFeedWithSingleUnderlying(address(priceFeed)).priceFeed() returns (address) {
58+
return NestingType.SINGLE_UNDERLYING;
59+
} catch {}
60+
61+
try NestedPriceFeedWithMultipleUnderlyings(address(priceFeed)).priceFeed0() returns (address) {
62+
return NestingType.MULTIPLE_UNDERLYING;
63+
} catch {}
64+
65+
return NestingType.NO_NESTING;
66+
}
67+
68+
function getSingleUnderlyingFeed(NestedPriceFeedWithSingleUnderlying priceFeed)
69+
internal
70+
view
71+
returns (address[] memory feeds, uint32[] memory stalenessPeriods)
72+
{
73+
feeds = new address[](1);
74+
stalenessPeriods = new uint32[](1);
75+
(feeds[0], stalenessPeriods[0]) = (priceFeed.priceFeed(), priceFeed.stalenessPeriod());
76+
}
77+
78+
function getMultipleUnderlyingFeeds(NestedPriceFeedWithMultipleUnderlyings priceFeed)
79+
internal
80+
view
81+
returns (address[] memory feeds, uint32[] memory stalenessPeriods)
82+
{
83+
feeds = new address[](MAX_UNDERLYING_PRICE_FEEDS);
84+
stalenessPeriods = new uint32[](MAX_UNDERLYING_PRICE_FEEDS);
85+
for (uint256 i; i < MAX_UNDERLYING_PRICE_FEEDS; ++i) {
86+
feeds[i] = _getPriceFeedByIndex(priceFeed, i);
87+
if (feeds[i] == address(0)) {
88+
assembly {
89+
mstore(feeds, i)
90+
mstore(stalenessPeriods, i)
91+
}
92+
break;
93+
}
94+
stalenessPeriods[i] = _getStalenessPeriodByIndex(priceFeed, i);
95+
}
96+
}
97+
98+
function _getPriceFeedByIndex(NestedPriceFeedWithMultipleUnderlyings priceFeed, uint256 index)
99+
private
100+
view
101+
returns (address)
102+
{
103+
bytes4 selector;
104+
if (index == 0) {
105+
selector = priceFeed.priceFeed0.selector;
106+
} else if (index == 1) {
107+
selector = priceFeed.priceFeed1.selector;
108+
} else if (index == 2) {
109+
selector = priceFeed.priceFeed2.selector;
110+
} else if (index == 3) {
111+
selector = priceFeed.priceFeed3.selector;
112+
} else if (index == 4) {
113+
selector = priceFeed.priceFeed4.selector;
114+
} else if (index == 5) {
115+
selector = priceFeed.priceFeed5.selector;
116+
} else if (index == 6) {
117+
selector = priceFeed.priceFeed6.selector;
118+
} else if (index == 7) {
119+
selector = priceFeed.priceFeed7.selector;
120+
}
121+
(bool success, bytes memory result) = address(priceFeed).staticcall(abi.encodePacked(selector));
122+
if (!success || result.length == 0) return address(0);
123+
return abi.decode(result, (address));
124+
}
125+
126+
function _getStalenessPeriodByIndex(NestedPriceFeedWithMultipleUnderlyings priceFeed, uint256 index)
127+
private
128+
view
129+
returns (uint32)
130+
{
131+
bytes4 selector;
132+
if (index == 0) {
133+
selector = priceFeed.stalenessPeriod0.selector;
134+
} else if (index == 1) {
135+
selector = priceFeed.stalenessPeriod1.selector;
136+
} else if (index == 2) {
137+
selector = priceFeed.stalenessPeriod2.selector;
138+
} else if (index == 3) {
139+
selector = priceFeed.stalenessPeriod3.selector;
140+
} else if (index == 4) {
141+
selector = priceFeed.stalenessPeriod4.selector;
142+
} else if (index == 5) {
143+
selector = priceFeed.stalenessPeriod5.selector;
144+
} else if (index == 6) {
145+
selector = priceFeed.stalenessPeriod6.selector;
146+
} else if (index == 7) {
147+
selector = priceFeed.stalenessPeriod7.selector;
148+
}
149+
(bool success, bytes memory result) = address(priceFeed).staticcall(abi.encodePacked(selector));
150+
if (!success || result.length == 0) return 0;
151+
return abi.decode(result, (uint32));
152+
}
153+
}

0 commit comments

Comments
 (0)