Skip to content

Commit 8e4d0c9

Browse files
committed
Optimizes for read on NormalizedApi3ReaderProxyV1 and ScaledApi3FeedProxyV1 while scaling feeds
1 parent fc21012 commit 8e4d0c9

File tree

6 files changed

+74
-30
lines changed

6 files changed

+74
-30
lines changed

contracts/NormalizedApi3ReaderProxyV1.sol

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,35 @@ contract NormalizedApi3ReaderProxyV1 is INormalizedApi3ReaderProxyV1 {
1515
/// @notice Chainlink AggregatorV2V3Interface contract address
1616
address public immutable override feed;
1717

18-
uint8 internal immutable feedDecimals;
18+
/// @dev Pre-calculated factor for scaling to 18 decimals.
19+
int256 private immutable scalingFactor;
20+
21+
/// @dev True for upscaling (multiply by scalingFactor), else downscaling
22+
/// (divide by scalingFactor).
23+
bool private immutable isUpscaling;
1924

2025
/// @param feed_ The address of the Chainlink AggregatorV2V3Interface feed
2126
constructor(address feed_) {
2227
if (feed_ == address(0)) {
2328
revert ZeroProxyAddress();
2429
}
25-
2630
uint8 feedDecimals_ = AggregatorV2V3Interface(feed_).decimals();
2731
if (feedDecimals_ == 0 || feedDecimals_ > 36) {
2832
revert UnsupportedFeedDecimals();
2933
}
34+
if (feedDecimals_ == 18) {
35+
revert NoNormalizationNeeded();
36+
}
3037
feed = feed_;
31-
feedDecimals = feedDecimals_;
38+
uint8 delta = feedDecimals_ > 18
39+
? feedDecimals_ - 18
40+
: 18 - feedDecimals_;
41+
scalingFactor = int256(10 ** uint256(delta));
42+
isUpscaling = feedDecimals_ < 18;
3243
}
3344

3445
/// @notice Returns the price of the underlying Chainlink feed normalized to
35-
/// 18 decimals
46+
/// 18 decimals.
3647
/// @return value The normalized signed fixed-point value with 18 decimals
3748
/// @return timestamp The updatedAt timestamp of the feed
3849
function read()
@@ -44,15 +55,9 @@ contract NormalizedApi3ReaderProxyV1 is INormalizedApi3ReaderProxyV1 {
4455
(, int256 answer, , uint256 updatedAt, ) = AggregatorV2V3Interface(feed)
4556
.latestRoundData();
4657

47-
if (feedDecimals != 18) {
48-
uint8 delta = feedDecimals > 18
49-
? feedDecimals - 18
50-
: 18 - feedDecimals;
51-
int256 factor = int256(10 ** uint256(delta));
52-
answer = feedDecimals < 18 ? answer * factor : answer / factor;
53-
}
54-
55-
value = int224(answer);
58+
value = isUpscaling
59+
? int224(answer * scalingFactor)
60+
: int224(answer / scalingFactor);
5661
timestamp = uint32(updatedAt);
5762
}
5863

contracts/adapters/ScaledApi3FeedProxyV1.sol

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,16 @@ contract ScaledApi3FeedProxyV1 is IScaledApi3FeedProxyV1 {
1313
/// @notice IApi3ReaderProxy contract address
1414
address public immutable override proxy;
1515

16+
/// @dev Target decimals for the scaled value.
1617
uint8 private immutable targetDecimals;
1718

19+
/// @dev Pre-calculated factor for scaling from 18 decimals.
20+
int256 private immutable scalingFactor;
21+
22+
/// @dev True for upscaling (multiply by scalingFactor), else downscaling
23+
/// (divide by scalingFactor).
24+
bool private immutable isUpscaling;
25+
1826
/// @param proxy_ IApi3ReaderProxy contract address
1927
/// @param targetDecimals_ Decimals used to scale the IApi3ReaderProxy value
2028
constructor(address proxy_, uint8 targetDecimals_) {
@@ -24,9 +32,16 @@ contract ScaledApi3FeedProxyV1 is IScaledApi3FeedProxyV1 {
2432
if (targetDecimals_ == 0 || targetDecimals_ > 36) {
2533
revert InvalidDecimals();
2634
}
27-
35+
if (targetDecimals_ == 18) {
36+
revert NoScalingNeeded();
37+
}
2838
proxy = proxy_;
2939
targetDecimals = targetDecimals_;
40+
uint8 delta = targetDecimals_ > 18
41+
? targetDecimals_ - 18
42+
: 18 - targetDecimals_;
43+
scalingFactor = int256(10 ** uint256(delta));
44+
isUpscaling = targetDecimals_ > 18;
3045
}
3146

3247
/// @dev AggregatorV2V3Interface users are already responsible with
@@ -124,22 +139,16 @@ contract ScaledApi3FeedProxyV1 is IScaledApi3FeedProxyV1 {
124139
/// performed using `int256` types. This allows the scaled result to exceed
125140
/// the `int224` range, provided it fits within `int256`.
126141
/// Arithmetic operations will revert on overflow or underflow
127-
/// (e.g., if `value * factor` exceeds `type(int256).max`).
142+
/// (e.g., if `value * scalingFactor` exceeds `type(int256).max`).
128143
/// @return value The scaled signed fixed-point value with `targetDecimals`.
129144
/// @return timestamp The timestamp from the underlying proxy.
130145
function _read() internal view returns (int256 value, uint32 timestamp) {
131146
(int224 proxyValue, uint32 proxyTimestamp) = IApi3ReaderProxy(proxy)
132147
.read();
133148

134-
value = proxyValue;
149+
value = isUpscaling
150+
? proxyValue * scalingFactor
151+
: proxyValue / scalingFactor;
135152
timestamp = proxyTimestamp;
136-
137-
if (18 != targetDecimals) {
138-
uint8 delta = 18 > targetDecimals
139-
? 18 - targetDecimals
140-
: targetDecimals - 18;
141-
int256 factor = int256(10 ** uint256(delta));
142-
value = 18 < targetDecimals ? value * factor : value / factor;
143-
}
144153
}
145154
}

contracts/adapters/interfaces/IScaledApi3FeedProxyV1.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ interface IScaledApi3FeedProxyV1 is AggregatorV2V3Interface {
88

99
error InvalidDecimals();
1010

11+
error NoScalingNeeded();
12+
1113
error FunctionIsNotSupported();
1214

1315
function proxy() external view returns (address proxy);

contracts/interfaces/INormalizedApi3ReaderProxyV1.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ interface INormalizedApi3ReaderProxyV1 is
1212

1313
error UnsupportedFeedDecimals();
1414

15+
error NoNormalizationNeeded();
16+
1517
error FunctionIsNotSupported();
1618

1719
function feed() external view returns (address feed);

test/NormalizedApi3ReaderProxyV1.sol.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,24 @@ describe('NormalizedApi3ReaderProxyV1', function () {
4242

4343
describe('constructor', function () {
4444
context('feed is not zero address', function () {
45-
it('constructs', async function () {
46-
const { feed, normalizedApi3ReaderProxyV1 } = await helpers.loadFixture(deploy);
47-
expect(await normalizedApi3ReaderProxyV1.feed()).to.equal(await feed.getAddress());
45+
context('feed does not have 18 decimals', function () {
46+
it('constructs', async function () {
47+
const { feed, normalizedApi3ReaderProxyV1 } = await helpers.loadFixture(deploy);
48+
expect(await normalizedApi3ReaderProxyV1.feed()).to.equal(await feed.getAddress());
49+
});
50+
});
51+
context('feed has 18 decimals', function () {
52+
it('reverts', async function () {
53+
const { roles, mockAggregatorV2V3Factory } = await helpers.loadFixture(deploy);
54+
const feed = await mockAggregatorV2V3Factory.deploy(18, ethers.parseEther('1'), await helpers.time.latest());
55+
const normalizedApi3ReaderProxyV1Factory = await ethers.getContractFactory(
56+
'NormalizedApi3ReaderProxyV1',
57+
roles.deployer
58+
);
59+
await expect(normalizedApi3ReaderProxyV1Factory.deploy(feed))
60+
.to.be.revertedWithCustomError(normalizedApi3ReaderProxyV1Factory, 'NoNormalizationNeeded')
61+
.withArgs();
62+
});
4863
});
4964
});
5065
context('feed is zero address', function () {

test/adapters/ScaledApi3FeedProxyV1.sol.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,20 @@ describe('ScaledApi3FeedProxyV1', function () {
8888
describe('constructor', function () {
8989
context('proxy is not zero address', function () {
9090
context('targetDecimals is not invalid', function () {
91-
it('constructs', async function () {
92-
const { api3ReaderProxyV1, scaledApi3FeedProxyV1 } = await helpers.loadFixture(deploy);
93-
expect(await scaledApi3FeedProxyV1.proxy()).to.equal(await api3ReaderProxyV1.getAddress());
91+
context('targetDecimals is not 18', function () {
92+
it('constructs', async function () {
93+
const { api3ReaderProxyV1, scaledApi3FeedProxyV1 } = await helpers.loadFixture(deploy);
94+
expect(await scaledApi3FeedProxyV1.proxy()).to.equal(await api3ReaderProxyV1.getAddress());
95+
});
96+
});
97+
context('targetDecimals is 18', function () {
98+
it('reverts', async function () {
99+
const { api3ReaderProxyV1, roles } = await helpers.loadFixture(deploy);
100+
const scaledApi3FeedProxyV1 = await ethers.getContractFactory('ScaledApi3FeedProxyV1', roles.deployer);
101+
await expect(scaledApi3FeedProxyV1.deploy(await api3ReaderProxyV1.getAddress(), 18))
102+
.to.be.revertedWithCustomError(scaledApi3FeedProxyV1, 'NoScalingNeeded')
103+
.withArgs();
104+
});
94105
});
95106
});
96107
context('targetDecimals is invalid', function () {

0 commit comments

Comments
 (0)