Skip to content

Commit 72be2a3

Browse files
authored
Review fixes (#16)
* feat: feed registry contract * fix: @hoytech's review comments
1 parent abbebc9 commit 72be2a3

File tree

18 files changed

+147
-200
lines changed

18 files changed

+147
-200
lines changed

src/EulerRouter.sol

Lines changed: 12 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ pragma solidity 0.8.23;
44
import {ERC4626} from "@solady/tokens/ERC4626.sol";
55
import {IPriceOracle} from "src/interfaces/IPriceOracle.sol";
66
import {Errors} from "src/lib/Errors.sol";
7+
import {Governable} from "src/lib/Governable.sol";
78

89
/// @title EulerRouter
910
/// @author Euler Labs (https://www.eulerlabs.com/)
1011
/// @notice Default Oracle resolver for Euler lending products.
11-
contract EulerRouter is IPriceOracle {
12+
contract EulerRouter is Governable, IPriceOracle {
1213
/// @notice The PriceOracle to call if this router is not configured for base/quote.
1314
/// @dev If `address(0)` then there is no fallback.
1415
address public fallbackOracle;
@@ -18,16 +19,14 @@ contract EulerRouter is IPriceOracle {
1819
/// @dev During resolution the vault is substituted with its asset.
1920
/// The `inAmount` is augmented by the vault's `convert*` function.
2021
mapping(address vault => address asset) public resolvedVaults;
21-
/// @notice The active governor address. If `address(0)` then the role is renounced.
22-
address public governor;
2322

24-
/// @notice Configure an PriceOracle to resolve base/quote.
23+
/// @notice Configure a PriceOracle to resolve base/quote.
2524
/// @param base The address of the base token.
2625
/// @param quote The address of the quote token.
2726
/// @param oracle The address of the PriceOracle that resolves base/quote.
2827
/// @dev If `oracle` is `address(0)` then the base/quote configuration was removed.
2928
event ConfigSet(address indexed base, address indexed quote, address indexed oracle);
30-
/// @notice Set an PriceOracle as a fallback resolver.
29+
/// @notice Set a PriceOracle as a fallback resolver.
3130
/// @param fallbackOracle The address of the PriceOracle that is called when base/quote is not configured.
3231
/// @dev If `fallbackOracle` is `address(0)` then there is no fallback resolver.
3332
event FallbackOracleSet(address indexed fallbackOracle);
@@ -36,18 +35,12 @@ contract EulerRouter is IPriceOracle {
3635
/// @param asset The address of the vault's asset.
3736
/// @dev If `asset` is `address(0)` then the configuration was removed.
3837
event ResolvedVaultSet(address indexed vault, address indexed asset);
39-
/// @notice Set the governor of the contract.
40-
/// @param oldGovernor The address of the previous governor.
41-
/// @param newGovernor The address of the newly appointed governor.
42-
event GovernorSet(address indexed oldGovernor, address indexed newGovernor);
4338

4439
/// @notice Deploy EulerRouter.
4540
/// @param _governor The address of the governor.
46-
constructor(address _governor) {
47-
_setGovernor(_governor);
48-
}
41+
constructor(address _governor) Governable(_governor) {}
4942

50-
/// @notice Configure an PriceOracle to resolve base/quote.
43+
/// @notice Configure a PriceOracle to resolve base/quote.
5144
/// @param base The address of the base token.
5245
/// @param quote The address of the quote token.
5346
/// @param oracle The address of the PriceOracle that resolves base/quote.
@@ -57,55 +50,26 @@ contract EulerRouter is IPriceOracle {
5750
emit ConfigSet(base, quote, oracle);
5851
}
5952

60-
/// @notice Clear the configuration for base/quote.
61-
/// @param base The address of the base token.
62-
/// @param quote The address of the quote token.
63-
/// @dev Callable only by the governor.
64-
function govClearConfig(address base, address quote) external onlyGovernor {
65-
delete oracles[base][quote];
66-
emit ConfigSet(base, quote, address(0));
67-
}
68-
6953
/// @notice Configure an ERC4626 vault to use internal pricing via `convert*` methods.
7054
/// @param vault The address of the ERC4626 vault.
55+
/// @param set True to configure the vault, false to clear the record.
7156
/// @dev Callable only by the governor. Vault must be ERC4626.
7257
/// Only configure internal pricing after verifying that the implementation of
7358
/// `convertToAssets` and `convertToShares` cannot be manipulated.
74-
function govSetResolvedVault(address vault) external onlyGovernor {
75-
address asset = ERC4626(vault).asset();
59+
function govSetResolvedVault(address vault, bool set) external onlyGovernor {
60+
address asset = set ? ERC4626(vault).asset() : address(0);
7661
resolvedVaults[vault] = asset;
7762
emit ResolvedVaultSet(vault, asset);
7863
}
7964

80-
/// @notice Clear the configuration for internal pricing resolution for a vault.
81-
/// @param vault The address of the ERC4626 vault.
82-
/// @dev Callable only by the governor.
83-
function govClearResolvedVault(address vault) external onlyGovernor {
84-
delete resolvedVaults[vault];
85-
emit ResolvedVaultSet(vault, address(0));
86-
}
87-
88-
/// @notice Set an PriceOracle as a fallback resolver.
65+
/// @notice Set a PriceOracle as a fallback resolver.
8966
/// @param _fallbackOracle The address of the PriceOracle that is called when base/quote is not configured.
9067
/// @dev `address(0)` removes the fallback.
9168
function govSetFallbackOracle(address _fallbackOracle) external onlyGovernor {
9269
fallbackOracle = _fallbackOracle;
9370
emit FallbackOracleSet(_fallbackOracle);
9471
}
9572

96-
/// @notice Transfer the governor role to another address.
97-
/// @param newGovernor The address of the next governor.
98-
/// @dev Can only be called by the current governor.
99-
function transferGovernance(address newGovernor) external onlyGovernor {
100-
_setGovernor(newGovernor);
101-
}
102-
103-
/// @notice Renounce the governor role.
104-
/// @dev Sets governor to address(0), effectively removing governance.
105-
function renounceGovernance() external onlyGovernor {
106-
_setGovernor(address(0));
107-
}
108-
10973
/// @inheritdoc IPriceOracle
11074
function getQuote(uint256 inAmount, address base, address quote) external view returns (uint256) {
11175
address oracle;
@@ -128,7 +92,7 @@ contract EulerRouter is IPriceOracle {
12892
/// @param quote The token that is the unit of account.
12993
/// @dev Implements the following recursive resolution logic:
13094
/// 1. Check the base case: `base == quote` and terminate if true.
131-
/// 2. If an PriceOracle is configured for base/quote in the `oracles` mapping,
95+
/// 2. If a PriceOracle is configured for base/quote in the `oracles` mapping,
13296
/// return it without transforming the other variables.
13397
/// 3. If `base` is configured as an ERC4626 vault with internal pricing,
13498
/// transform inAmount by calling `convertToAssets` and recurse by substituting `asset` for `base`.
@@ -137,7 +101,7 @@ contract EulerRouter is IPriceOracle {
137101
/// 5. If there is a fallback oracle, return it without transforming the other variables, else revert.
138102
/// @return The resolved inAmount.
139103
/// @return The resolved base.
140-
/// @return The resolved base.
104+
/// @return The resolved quote.
141105
/// @return The resolved PriceOracle to call.
142106
function _resolveOracle(uint256 inAmount, address base, address quote)
143107
internal
@@ -166,20 +130,4 @@ contract EulerRouter is IPriceOracle {
166130
if (oracle == address(0)) revert Errors.PriceOracle_NotSupported(base, quote);
167131
return (inAmount, base, quote, oracle);
168132
}
169-
170-
/// @notice Set the governor address.
171-
/// @param newGovernor The address of the new governor.
172-
function _setGovernor(address newGovernor) internal {
173-
address oldGovernor = governor;
174-
governor = newGovernor;
175-
emit GovernorSet(oldGovernor, newGovernor);
176-
}
177-
178-
/// @notice Restrict access to the governor.
179-
modifier onlyGovernor() {
180-
if (msg.sender != governor) {
181-
revert Errors.Governance_CallerNotGovernor();
182-
}
183-
_;
184-
}
185133
}

src/FeedRegistry.sol

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity 0.8.23;
3+
4+
import {Errors} from "src/lib/Errors.sol";
5+
import {Governable} from "src/lib/Governable.sol";
6+
7+
contract FeedRegistry is Governable {
8+
address public immutable quote;
9+
mapping(bytes32 feedId => address base) public feeds;
10+
11+
event FeedSet(bytes32 indexed feedId, address indexed base);
12+
13+
constructor(address _governor, address _quote) Governable(_governor) {
14+
quote = _quote;
15+
}
16+
17+
function setFeeds(bytes32[] calldata feedIds, address[] calldata bases) external onlyGovernor {
18+
_setFeeds(feedIds, bases);
19+
}
20+
21+
function _setFeeds(bytes32[] calldata _feedIds, address[] calldata _bases) internal {
22+
if (_feedIds.length != _bases.length) revert Errors.PriceOracle_InvalidConfiguration();
23+
for (uint256 i = 0; i < _feedIds.length; ++i) {
24+
_setFeed(_feedIds[i], _bases[i]);
25+
}
26+
}
27+
28+
function _setFeed(bytes32 _feedId, address _base) internal {
29+
if (feeds[_feedId] != address(0)) revert Errors.PriceOracle_InvalidConfiguration();
30+
feeds[_feedId] = _base;
31+
emit FeedSet(_feedId, _base);
32+
}
33+
}

src/adapter/chainlink/ChainlinkOracle.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: UNLICENSED
22
pragma solidity 0.8.23;
33

4-
import {ERC20} from "@solady/tokens/ERC20.sol";
4+
import {IERC20} from "forge-std/interfaces/IERC20.sol";
55
import {BaseAdapter} from "src/adapter/BaseAdapter.sol";
66
import {AggregatorV3Interface} from "src/adapter/chainlink/AggregatorV3Interface.sol";
77
import {Errors} from "src/lib/Errors.sol";
@@ -43,8 +43,8 @@ contract ChainlinkOracle is BaseAdapter {
4343
inverse = _inverse;
4444

4545
// The scale factor is used to correctly convert decimals.
46-
int8 baseDecimals = int8(ERC20(base).decimals());
47-
int8 quoteDecimals = int8(ERC20(quote).decimals());
46+
int8 baseDecimals = int8(IERC20(base).decimals());
47+
int8 quoteDecimals = int8(IERC20(quote).decimals());
4848
int8 feedDecimals = int8(AggregatorV3Interface(feed).decimals());
4949
int8 scaleDecimals;
5050
if (inverse) {

src/adapter/pyth/PythOracle.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// SPDX-License-Identifier: UNLICENSED
22
pragma solidity 0.8.23;
33

4+
import {IERC20} from "forge-std/interfaces/IERC20.sol";
45
import {IPyth} from "@pyth/IPyth.sol";
56
import {PythStructs} from "@pyth/PythStructs.sol";
6-
import {ERC20} from "@solady/tokens/ERC20.sol";
77
import {BaseAdapter} from "src/adapter/BaseAdapter.sol";
88
import {Errors} from "src/lib/Errors.sol";
99

@@ -44,8 +44,8 @@ contract PythOracle is BaseAdapter {
4444
feedId = _feedId;
4545
maxStaleness = _maxStaleness;
4646
inverse = _inverse;
47-
uint8 baseDecimals = ERC20(_base).decimals();
48-
uint8 quoteDecimals = ERC20(_quote).decimals();
47+
uint8 baseDecimals = IERC20(_base).decimals();
48+
uint8 quoteDecimals = IERC20(_quote).decimals();
4949
scaleExponent = inverse ? int8(baseDecimals) - int8(quoteDecimals) : int8(quoteDecimals) - int8(baseDecimals);
5050
}
5151

src/adapter/redstone/RedstoneCoreOracle.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// SPDX-License-Identifier: UNLICENSED
22
pragma solidity 0.8.23;
33

4+
import {IERC20} from "forge-std/interfaces/IERC20.sol";
45
import {RedstoneDefaultsLib} from "@redstone/evm-connector/core/RedstoneDefaultsLib.sol";
56
import {PrimaryProdDataServiceConsumerBase} from
67
"@redstone/evm-connector/data-services/PrimaryProdDataServiceConsumerBase.sol";
7-
import {ERC20} from "@solady/tokens/ERC20.sol";
88
import {BaseAdapter} from "src/adapter/BaseAdapter.sol";
99
import {Errors} from "src/lib/Errors.sol";
1010

@@ -29,10 +29,10 @@ contract RedstoneCoreOracle is PrimaryProdDataServiceConsumerBase, BaseAdapter {
2929
uint256 internal immutable scaleFactor;
3030
/// @notice The last updated price.
3131
/// @dev This gets updated after calling `updatePrice`.
32-
uint224 public lastPrice;
32+
uint208 public lastPrice;
3333
/// @notice The timestamp of the last update.
3434
/// @dev Gets updated ot `block.timestamp` after calling `updatePrice`.
35-
uint32 public lastUpdatedAt;
35+
uint48 public lastUpdatedAt;
3636

3737
/// @notice Deploy a RedstoneCoreOracle.
3838
/// @param _base The address of the base asset corresponding to the feed.
@@ -53,7 +53,7 @@ contract RedstoneCoreOracle is PrimaryProdDataServiceConsumerBase, BaseAdapter {
5353
maxStaleness = _maxStaleness;
5454
inverse = _inverse;
5555

56-
uint8 decimals = ERC20(inverse ? _quote : _base).decimals();
56+
uint8 decimals = IERC20(inverse ? _quote : _base).decimals();
5757
scaleFactor = 10 ** decimals;
5858
}
5959

@@ -64,9 +64,9 @@ contract RedstoneCoreOracle is PrimaryProdDataServiceConsumerBase, BaseAdapter {
6464
if (block.timestamp < lastUpdatedAt + maxStaleness) return;
6565

6666
uint256 price = getOracleNumericValueFromTxMsg(feedId);
67-
if (price > type(uint224).max) revert Errors.PriceOracle_Overflow();
68-
lastPrice = uint224(price);
69-
lastUpdatedAt = uint32(block.timestamp);
67+
if (price > type(uint208).max) revert Errors.PriceOracle_Overflow();
68+
lastPrice = uint208(price);
69+
lastUpdatedAt = uint48(block.timestamp);
7070
}
7171

7272
/// @notice Get the quote from the Redstone feed.

src/adapter/uniswap/UniswapV3Oracle.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ contract UniswapV3Oracle is BaseAdapter {
4343
}
4444

4545
/// @notice Get a quote by calling the pool's TWAP oracle.
46-
/// @dev Supports spot pricing if twapWindow=0.
4746
/// @param inAmount The amount of `base` to convert.
4847
/// @param base The token that is being priced. Either `tokenA` or `tokenB`.
4948
/// @param quote The token that is the unit of account. Either `tokenB` or `tokenA`.

src/interfaces/IFactoryInitializable.sol

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ interface IFactoryInitializable {
1515
/// @param newGovernor The address of the next governor.
1616
/// @dev Can only be called by the current governor.
1717
function transferGovernance(address newGovernor) external;
18-
/// @notice Remove the governor.
19-
/// @dev Sets governor to address(0), effectively removing governance.
20-
function renounceGovernance() external;
2118
/// @notice Check whether the contract has been initialized.
2219
/// @return Whether `initialize` has been called.
2320
function initialized() external view returns (bool);

src/lib/Governable.sol

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity 0.8.23;
3+
4+
import {Errors} from "src/lib/Errors.sol";
5+
6+
/// @title Governable
7+
/// @author Euler Labs (https://www.eulerlabs.com/)
8+
/// @notice Contract mixin for governance.
9+
abstract contract Governable {
10+
/// @notice The active governor address. If `address(0)` then the role is renounced.
11+
address public governor;
12+
13+
/// @notice Set the governor of the contract.
14+
/// @param oldGovernor The address of the previous governor.
15+
/// @param newGovernor The address of the newly appointed governor.
16+
event GovernorSet(address indexed oldGovernor, address indexed newGovernor);
17+
18+
constructor(address _governor) {
19+
_setGovernor(_governor);
20+
}
21+
22+
/// @notice Transfer the governor role to another address.
23+
/// @param newGovernor The address of the next governor.
24+
/// @dev Can only be called by the current governor.
25+
function transferGovernance(address newGovernor) external onlyGovernor {
26+
_setGovernor(newGovernor);
27+
}
28+
29+
/// @notice Restrict access to the governor.
30+
modifier onlyGovernor() {
31+
if (msg.sender != governor) {
32+
revert Errors.Governance_CallerNotGovernor();
33+
}
34+
_;
35+
}
36+
37+
/// @notice Set the governor address.
38+
/// @param newGovernor The address of the new governor.
39+
function _setGovernor(address newGovernor) internal {
40+
address oldGovernor = governor;
41+
governor = newGovernor;
42+
emit GovernorSet(oldGovernor, newGovernor);
43+
}
44+
}

test/fork/adapter/lido/LidoOracle.fork.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ contract LidoOracleForkTest is ForkTest {
1414
oracle = new LidoOracle(STETH, WSTETH);
1515
}
1616

17-
function test_GetQuote_Integrity() public {
17+
function test_GetQuote_Integrity() public view {
1818
uint256 stEthWstEth = oracle.getQuote(1e18, STETH, WSTETH);
1919
assertApproxEqRel(stEthWstEth, 0.85e18, 0.1e18);
2020

2121
uint256 wstEthStEth = oracle.getQuote(1e18, WSTETH, STETH);
2222
assertApproxEqRel(wstEthStEth, 1.15e18, 0.1e18);
2323
}
2424

25-
function test_GetQuotes_Integrity() public {
25+
function test_GetQuotes_Integrity() public view {
2626
(uint256 stEthWstEthBid, uint256 stEthWstEthAsk) = oracle.getQuotes(1e18, STETH, WSTETH);
2727
assertApproxEqRel(stEthWstEthBid, 0.85e18, 0.1e18);
2828
assertApproxEqRel(stEthWstEthAsk, 0.85e18, 0.1e18);

test/fork/adapter/rocketpool/RethOracle.fork.t.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ contract RethOracleForkTest is ForkTest {
1414
oracle = new RethOracle(WETH, RETH);
1515
}
1616

17-
function test_GetQuote_Integrity() public {
17+
function test_GetQuote_Integrity() public view {
1818
uint256 wethReth = oracle.getQuote(1e18, WETH, RETH);
1919
assertApproxEqRel(wethReth, 0.9e18, 0.1e18);
2020

2121
uint256 rethWeth = oracle.getQuote(1e18, RETH, WETH);
2222
assertApproxEqRel(rethWeth, 1.1e18, 0.1e18);
2323
}
2424

25-
function test_GetQuotes_Integrity() public {
25+
function test_GetQuotes_Integrity() public view {
2626
(uint256 wethRethBid, uint256 wethRethAsk) = oracle.getQuotes(1e18, WETH, RETH);
2727
assertApproxEqRel(wethRethBid, 0.9e18, 0.1e18);
2828
assertApproxEqRel(wethRethAsk, 0.9e18, 0.1e18);

0 commit comments

Comments
 (0)