Skip to content

Commit ac97b4d

Browse files
authored
[eth] - Aave FallbackOracle Integration (#924)
* feat(eth): aave integration Add IPriceOracleGetter and PythAssetRegistry mapping * feat(eth): remove IPriceOracleGetter from PythAssetRegistryGetter * refactor(eth): flatten PythAssetRegistySetter/Getter into PythAssetRegistry * feat(eth): address feedback move aave related contracts into separate directory, add explicit exponent/decimal handling, add staleness check * refactor(eth): minor rename to avoid shadowing * fix(eth): handle exponent conversion and add tests * chore(eth): remove unused console import * feat(eth): address PR feedback add more checks, tests & minor refactoring * feat(eth): add more tests and address feedback
1 parent aa0e6fd commit ac97b4d

File tree

8 files changed

+612
-4
lines changed

8 files changed

+612
-4
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// SPDX-License-Identifier: Apache 2
2+
pragma solidity ^0.8.0;
3+
4+
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
5+
6+
error InconsistentParamsLength();
7+
8+
contract PythAssetRegistryStorage {
9+
struct State {
10+
address pyth;
11+
address BASE_CURRENCY;
12+
uint256 BASE_CURRENCY_UNIT;
13+
// Map of asset priceIds (asset => priceId)
14+
mapping(address => bytes32) assetsPriceIds;
15+
/// Maximum acceptable time period before price is considered to be stale.
16+
/// This includes attestation delay, block time, and potential clock drift
17+
/// between the source/target chains.
18+
uint validTimePeriodSeconds;
19+
}
20+
}
21+
22+
contract PythAssetRegistry {
23+
PythAssetRegistryStorage.State _registryState;
24+
25+
/**
26+
* @dev Emitted after the base currency is set
27+
* @param baseCurrency The base currency of used for price quotes
28+
* @param baseCurrencyUnit The unit of the base currency
29+
*/
30+
event BaseCurrencySet(
31+
address indexed baseCurrency,
32+
uint256 baseCurrencyUnit
33+
);
34+
35+
/**
36+
* @dev Emitted after the price source of an asset is updated
37+
* @param asset The address of the asset
38+
* @param source The priceId of the asset
39+
*/
40+
event AssetSourceUpdated(address indexed asset, bytes32 indexed source);
41+
42+
function pyth() public view returns (IPyth) {
43+
return IPyth(_registryState.pyth);
44+
}
45+
46+
function setPyth(address pythAddress) internal {
47+
_registryState.pyth = payable(pythAddress);
48+
}
49+
50+
function setAssetsSources(
51+
address[] memory assets,
52+
bytes32[] memory priceIds
53+
) internal {
54+
if (assets.length != priceIds.length) {
55+
revert InconsistentParamsLength();
56+
}
57+
for (uint256 i = 0; i < assets.length; i++) {
58+
_registryState.assetsPriceIds[assets[i]] = priceIds[i];
59+
emit AssetSourceUpdated(assets[i], priceIds[i]);
60+
}
61+
}
62+
63+
function setBaseCurrency(
64+
address baseCurrency,
65+
uint256 baseCurrencyUnit
66+
) internal {
67+
_registryState.BASE_CURRENCY = baseCurrency;
68+
_registryState.BASE_CURRENCY_UNIT = baseCurrencyUnit;
69+
emit BaseCurrencySet(baseCurrency, baseCurrencyUnit);
70+
}
71+
72+
function setValidTimePeriodSeconds(uint validTimePeriodInSeconds) internal {
73+
_registryState.validTimePeriodSeconds = validTimePeriodInSeconds;
74+
}
75+
76+
function validTimePeriodSeconds() public view returns (uint) {
77+
return _registryState.validTimePeriodSeconds;
78+
}
79+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// SPDX-License-Identifier: Apache 2
2+
pragma solidity ^0.8.0;
3+
4+
import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
5+
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
6+
7+
import "./interfaces/IPriceOracleGetter.sol";
8+
import "./PythAssetRegistry.sol";
9+
10+
/// Invalid non-positive price
11+
error InvalidNonPositivePrice();
12+
/// Normalization overflow
13+
error NormalizationOverflow();
14+
/// Invalid Base Currency Unit value. Must be power of 10.
15+
error InvalidBaseCurrencyUnit();
16+
17+
contract PythPriceOracleGetter is PythAssetRegistry, IPriceOracleGetter {
18+
/// @inheritdoc IPriceOracleGetter
19+
address public immutable override BASE_CURRENCY;
20+
/**
21+
* @notice Returns the base currency unit
22+
* @dev 1 ether for ETH, 1e8 for USD.
23+
* @return Returns the base currency unit.
24+
*/
25+
uint256 public immutable override BASE_CURRENCY_UNIT;
26+
/// BASE_CURRENCY_UNIT as a power of 10
27+
uint8 public immutable BASE_NUM_DECIMALS;
28+
29+
constructor(
30+
address pyth,
31+
address[] memory assets,
32+
bytes32[] memory priceIds,
33+
address baseCurrency,
34+
uint256 baseCurrencyUnit,
35+
uint validTimePeriodSeconds
36+
) {
37+
if (baseCurrencyUnit == 0) {
38+
revert InvalidBaseCurrencyUnit();
39+
}
40+
PythAssetRegistry.setPyth(pyth);
41+
PythAssetRegistry.setAssetsSources(assets, priceIds);
42+
PythAssetRegistry.setBaseCurrency(baseCurrency, baseCurrencyUnit);
43+
BASE_CURRENCY = _registryState.BASE_CURRENCY;
44+
BASE_CURRENCY_UNIT = _registryState.BASE_CURRENCY_UNIT;
45+
if ((10 ** baseNumDecimals(baseCurrencyUnit)) != baseCurrencyUnit) {
46+
revert InvalidBaseCurrencyUnit();
47+
}
48+
BASE_NUM_DECIMALS = baseNumDecimals(baseCurrencyUnit);
49+
PythAssetRegistry.setValidTimePeriodSeconds(validTimePeriodSeconds);
50+
}
51+
52+
/// @inheritdoc IPriceOracleGetter
53+
function getAssetPrice(
54+
address asset
55+
) external view override returns (uint256) {
56+
bytes32 priceId = _registryState.assetsPriceIds[asset];
57+
if (asset == BASE_CURRENCY) {
58+
return BASE_CURRENCY_UNIT;
59+
}
60+
if (priceId == 0) {
61+
revert PythErrors.PriceFeedNotFound();
62+
}
63+
PythStructs.Price memory price = pyth().getPriceNoOlderThan(
64+
priceId,
65+
PythAssetRegistry.validTimePeriodSeconds()
66+
);
67+
68+
// Aave is not using any price feeds < 0 for now.
69+
if (price.price <= 0) {
70+
revert InvalidNonPositivePrice();
71+
}
72+
uint256 normalizedPrice = uint64(price.price);
73+
int32 normalizerExpo = price.expo + int8(BASE_NUM_DECIMALS);
74+
bool isNormalizerExpoNeg = normalizerExpo < 0;
75+
uint256 normalizer = isNormalizerExpoNeg
76+
? 10 ** uint32(-normalizerExpo)
77+
: 10 ** uint32(normalizerExpo);
78+
79+
// this check prevents overflow in normalized price
80+
if (!isNormalizerExpoNeg && normalizer > type(uint192).max) {
81+
revert NormalizationOverflow();
82+
}
83+
84+
normalizedPrice = isNormalizerExpoNeg
85+
? normalizedPrice / normalizer
86+
: normalizedPrice * normalizer;
87+
88+
if (normalizedPrice <= 0) {
89+
revert InvalidNonPositivePrice();
90+
}
91+
92+
return normalizedPrice;
93+
}
94+
95+
function baseNumDecimals(uint number) private pure returns (uint8) {
96+
uint8 digits = 0;
97+
while (number != 0) {
98+
number /= 10;
99+
digits++;
100+
}
101+
return digits - 1;
102+
}
103+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// contracts/pyth/aave/PythPriceOracleGetter.sol
2+
// SPDX-License-Identifier: AGPL-3.0
3+
pragma solidity ^0.8.0;
4+
5+
/**
6+
* @title IPriceOracleGetter
7+
* @author Aave
8+
* @notice Interface for the Aave price oracle.
9+
*/
10+
interface IPriceOracleGetter {
11+
/**
12+
* @notice Returns the base currency address
13+
* @dev Address 0x0 is reserved for USD as base currency.
14+
* @return Returns the base currency address.
15+
*/
16+
function BASE_CURRENCY() external view returns (address);
17+
18+
/**
19+
* @notice Returns the base currency unit
20+
* @dev 1 ether for ETH, 1e8 for USD.
21+
* @return Returns the base currency unit.
22+
*/
23+
function BASE_CURRENCY_UNIT() external view returns (uint256);
24+
25+
/**
26+
* @notice Returns the asset price in the base currency
27+
* @param asset The address of the asset
28+
* @return The price of the asset
29+
*/
30+
function getAssetPrice(address asset) external view returns (uint256);
31+
}

target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
4949
uint[] freshPricesWhMerkleUpdateFee; // i th element contains the update fee for the first i prices
5050

5151
uint64 sequence;
52-
uint randSeed;
52+
uint randomSeed;
5353

5454
function setUp() public {
5555
address wormholeAddr = setUpWormholeReceiver(NUM_GUARDIANS);
@@ -120,8 +120,8 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
120120
}
121121

122122
function getRand() internal returns (uint val) {
123-
++randSeed;
124-
val = uint(keccak256(abi.encode(randSeed)));
123+
++randomSeed;
124+
val = uint(keccak256(abi.encode(randomSeed)));
125125
}
126126

127127
function generateWhBatchUpdateDataAndFee(

0 commit comments

Comments
 (0)