Skip to content

Commit e0762f5

Browse files
authored
feat: add scaled price adapter (#23)
* feat: scaled price adaptor * chore: rename, negative constructor test * chore: price feed decimal * chore: nits on natspec, mv scale scaleFactor * fix: fuzz bounds, reorder docs
1 parent 2ac5c2f commit e0762f5

File tree

6 files changed

+156
-4
lines changed

6 files changed

+156
-4
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.10;
3+
4+
import {AggregatorInterface} from '../../dependencies/chainlink/AggregatorInterface.sol';
5+
import {IScaledPriceAdapter} from '../../interfaces/IScaledPriceAdapter.sol';
6+
7+
/**
8+
* @title ScaledPriceAdapter
9+
* @author Aave Labs
10+
* @dev Price Adapter for Chainlink price feeds to scale price to standard USD unit of 8 decimals.
11+
*/
12+
contract ScaledPriceAdapter is IScaledPriceAdapter {
13+
AggregatorInterface internal immutable _SOURCE;
14+
15+
uint8 internal constant _BASE_DECIMALS = 8;
16+
bool internal immutable _SCALE_UP;
17+
uint256 internal immutable _SCALE_FACTOR;
18+
19+
constructor(address source_) {
20+
_SOURCE = AggregatorInterface(source_);
21+
uint8 sourceDecimals = _SOURCE.decimals();
22+
_SCALE_UP = sourceDecimals < _BASE_DECIMALS;
23+
_SCALE_FACTOR =
24+
10 ** (_SCALE_UP ? _BASE_DECIMALS - sourceDecimals : sourceDecimals - _BASE_DECIMALS);
25+
}
26+
27+
/// @inheritdoc IScaledPriceAdapter
28+
function latestAnswer() external view returns (int256) {
29+
return
30+
_SCALE_UP
31+
? _SOURCE.latestAnswer() * int256(_SCALE_FACTOR)
32+
: _SOURCE.latestAnswer() / int256(_SCALE_FACTOR);
33+
}
34+
35+
/// @inheritdoc IScaledPriceAdapter
36+
function description() external view returns (string memory) {
37+
return string.concat(_SOURCE.description(), ' (USD Scaled)');
38+
}
39+
40+
/// @inheritdoc IScaledPriceAdapter
41+
function decimals() external pure returns (uint8) {
42+
return _BASE_DECIMALS;
43+
}
44+
45+
/// @inheritdoc IScaledPriceAdapter
46+
function scale() external view returns (bool, uint256) {
47+
return (_SCALE_UP, _SCALE_FACTOR);
48+
}
49+
50+
/// @inheritdoc IScaledPriceAdapter
51+
function source() external view returns (address) {
52+
return address(_SOURCE);
53+
}
54+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.10;
3+
4+
interface IScaledPriceAdapter {
5+
/**
6+
* @dev Returns the feed decimals.
7+
*/
8+
function decimals() external view returns (uint8);
9+
10+
/**
11+
* @dev Returns the description of the feed.
12+
*/
13+
function description() external view returns (string memory);
14+
15+
/**
16+
* @dev Scaled `latestAnswer` from chainlink price feed.
17+
* @dev Loses price precision by log10(scaleFactor) when scaling down.
18+
*/
19+
function latestAnswer() external view returns (int256);
20+
21+
/**
22+
* @dev Direction and units used to scale latestAnswer to base decimals of 8.
23+
* @dev Loses price precision by log10(scaleFactor) when scaling down.
24+
* @return scaleUp Whether to scale up or down.
25+
* @return scaleFactor The units to scale by.
26+
*/
27+
function scale() external view returns (bool scaleUp, uint256 scaleFactor);
28+
29+
/**
30+
* @dev Underlying chainlink price feed.
31+
*/
32+
function source() external view returns (address);
33+
}

src/contracts/mocks/oracle/CLAggregators/MockAggregator.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ contract MockAggregator {
1919
return 1;
2020
}
2121

22-
function decimals() external pure returns (uint8) {
22+
function decimals() external view virtual returns (uint8) {
2323
return 8;
2424
}
2525
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.10;
3+
4+
import {MockAggregator} from './MockAggregator.sol';
5+
6+
contract MockAggregatorMetadata is MockAggregator {
7+
uint8 internal immutable _DECIMALS;
8+
9+
constructor(int256 initialAnswer_, uint8 decimals_) MockAggregator(initialAnswer_) {
10+
_DECIMALS = decimals_;
11+
}
12+
13+
function decimals() external view override returns (uint8) {
14+
return _DECIMALS;
15+
}
16+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
import {MockAggregatorMetadata} from '../../../src/contracts/mocks/oracle/CLAggregators/MockAggregatorMetadata.sol';
5+
import {ScaledPriceAdapter} from '../../../src/contracts/extensions/price-adapters/ScaledPriceAdapter.sol';
6+
import {TestnetProcedures} from '../../utils/TestnetProcedures.sol';
7+
8+
contract ScaledPriceAdapterTests is TestnetProcedures {
9+
function test_adapter_less_than_base() public {
10+
test_fuzz_adapter({sourceDecimals: 2, price: 1e2});
11+
test_fuzz_adapter({sourceDecimals: 6, price: 32.323e6});
12+
}
13+
14+
function test_adapter_greater_than_base() public {
15+
test_fuzz_adapter({sourceDecimals: 12, price: 1e12});
16+
}
17+
18+
function test_adapter_equal_to_base() public {
19+
test_fuzz_adapter({sourceDecimals: 8, price: 1e8});
20+
}
21+
22+
function test_fuzz_adapter(uint256 sourceDecimals, int256 price) public {
23+
sourceDecimals = bound(sourceDecimals, 1, 36);
24+
price = bound(price, 0, int256(10 ** (10 + sourceDecimals)));
25+
address source = address(new MockAggregatorMetadata(price, uint8(sourceDecimals)));
26+
ScaledPriceAdapter adapter = new ScaledPriceAdapter(source);
27+
28+
(bool scaleUp, uint256 scale) = adapter.scale();
29+
assertEq(adapter.decimals(), 8);
30+
assertEq(scaleUp, adapter.decimals() > sourceDecimals);
31+
assertEq(
32+
scale,
33+
10 ** (scaleUp ? adapter.decimals() - sourceDecimals : sourceDecimals - adapter.decimals())
34+
);
35+
assertEq(adapter.latestAnswer(), scaleUp ? price * int256(scale) : price / int256(scale));
36+
assertEq(adapter.source(), source);
37+
}
38+
39+
function test_adapter_invalid_source_feed() public {
40+
vm.expectRevert();
41+
new ScaledPriceAdapter(makeAddr('invalid'));
42+
}
43+
}

tests/mocks/AaveV3TestListing.sol

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
pragma solidity ^0.8.0;
33

44
import '../../src/contracts/extensions/v3-config-engine/AaveV3Payload.sol';
5+
import {ScaledPriceAdapter} from '../../src/contracts/extensions/price-adapters/ScaledPriceAdapter.sol';
56
import {TestnetRwaERC20} from '../../src/contracts/mocks/testnet-helpers/TestnetRwaERC20.sol';
67
import {TestnetERC20} from '../../src/contracts/mocks/testnet-helpers/TestnetERC20.sol';
78
import {MockAggregator} from '../../src/contracts/mocks/oracle/CLAggregators/MockAggregator.sol';
9+
import {MockAggregatorMetadata} from '../../src/contracts/mocks/oracle/CLAggregators/MockAggregatorMetadata.sol';
810
import {ACLManager} from '../../src/contracts/protocol/configuration/ACLManager.sol';
911
import {MarketReport} from '../../src/deployments/interfaces/IMarketReportTypes.sol';
1012
import {IPoolConfigurator, ConfiguratorInputTypes} from '../../src/contracts/interfaces/IPoolConfigurator.sol';
@@ -62,13 +64,17 @@ contract AaveV3TestListing is AaveV3Payload {
6264
WETH_MOCK_PRICE_FEED = address(new MockAggregator(1800e8));
6365

6466
BUIDL_ADDRESS = address(new TestnetRwaERC20('BUIDL', 'BUIDL', 6, erc20Owner));
65-
BUIDL_MOCK_PRICE_FEED = address(new MockAggregator(1e8));
67+
BUIDL_MOCK_PRICE_FEED = address(
68+
new ScaledPriceAdapter(address(new MockAggregatorMetadata(1e2, 2)))
69+
);
6670

6771
USTB_ADDRESS = address(new TestnetRwaERC20('USTB', 'USTB', 6, erc20Owner));
68-
USTB_MOCK_PRICE_FEED = address(new MockAggregator(10e8));
72+
USTB_MOCK_PRICE_FEED = address(
73+
new ScaledPriceAdapter(address(new MockAggregatorMetadata(10e6, 6)))
74+
);
6975

7076
WTGXX_ADDRESS = address(new TestnetRwaERC20('WTGXX', 'WTGXX', 18, erc20Owner));
71-
WTGXX_MOCK_PRICE_FEED = address(new MockAggregator(1e8));
77+
WTGXX_MOCK_PRICE_FEED = address(new ScaledPriceAdapter(address(new MockAggregator(1e8))));
7278

7379
GHO_ADDRESS = address(new TestnetERC20('GHO', 'GHO', 18, erc20Owner));
7480
GHO_MOCK_PRICE_FEED = address(new MockAggregator(1e8));

0 commit comments

Comments
 (0)