Skip to content

Commit ddbc5a1

Browse files
authored
Merge pull request #7 from api3dao/proxy-utils-removal
Removes ProxyUtils library
2 parents 57009b5 + 9cc4ffe commit ddbc5a1

File tree

7 files changed

+116
-60
lines changed

7 files changed

+116
-60
lines changed

contracts/NormalizedApi3ReaderProxyV1.sol

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,58 @@
22
pragma solidity ^0.8.27;
33

44
import "./interfaces/INormalizedApi3ReaderProxyV1.sol";
5-
import "./ProxyUtils.sol";
65

76
/// @title An immutable proxy contract that converts a Chainlink
87
/// AggregatorV2V3Interface feed output to 18 decimals to conform with
9-
/// IApi3ReaderProxy decimal standard
8+
/// IApi3ReaderProxy decimal convention
109
/// @dev This contract implements the AggregatorV2V3Interface to be compatible
1110
/// with Chainlink aggregators. This allows the contract to be used as a drop-in
1211
/// replacement for Chainlink aggregators in existing dApps.
1312
/// Refer to https://github.com/api3dao/migrate-from-chainlink-to-api3 for more
1413
/// information about the Chainlink interface implementation.
1514
contract NormalizedApi3ReaderProxyV1 is INormalizedApi3ReaderProxyV1 {
16-
using ProxyUtils for int256;
17-
1815
/// @notice Chainlink AggregatorV2V3Interface contract address
1916
address public immutable override feed;
2017

21-
uint8 internal immutable feedDecimals;
18+
/// @notice Pre-calculated factor for scaling the feed's value to 18
19+
/// decimals.
20+
int256 public immutable scalingFactor;
21+
22+
/// @notice True if upscaling (multiply by `scalingFactor`), false if
23+
/// downscaling (divide by `scalingFactor`), to normalize to 18 decimals.
24+
bool public immutable isUpscaling;
2225

2326
/// @param feed_ The address of the Chainlink AggregatorV2V3Interface feed
2427
constructor(address feed_) {
2528
if (feed_ == address(0)) {
2629
revert ZeroProxyAddress();
2730
}
28-
2931
uint8 feedDecimals_ = AggregatorV2V3Interface(feed_).decimals();
3032
if (feedDecimals_ == 0 || feedDecimals_ > 36) {
3133
revert UnsupportedFeedDecimals();
3234
}
35+
if (feedDecimals_ == 18) {
36+
revert NoNormalizationNeeded();
37+
}
3338
feed = feed_;
34-
feedDecimals = feedDecimals_;
39+
uint8 delta = feedDecimals_ > 18
40+
? feedDecimals_ - 18
41+
: 18 - feedDecimals_;
42+
scalingFactor = int256(10 ** uint256(delta));
43+
isUpscaling = feedDecimals_ < 18;
3544
}
3645

3746
/// @notice Returns the price of the underlying Chainlink feed normalized to
38-
/// 18 decimals
39-
/// of underlying Chainlink feed
47+
/// 18 decimals.
48+
/// @dev Fetches an `int256` answer from the Chainlink feed and scales it
49+
/// to 18 decimals using pre-calculated factors. The result is cast to
50+
/// `int224` to conform to the `IApi3ReaderProxy` interface.
51+
/// IMPORTANT: If the normalized `int256` value is outside the `int224`
52+
/// range, this cast causes silent truncation and data loss. Deployers
53+
/// must verify that the source feed's characteristics (value magnitude
54+
/// and original decimals) ensure the 18-decimal normalized value fits
55+
/// `int224`. Scaling arithmetic (prior to cast) reverts on `int256`
56+
/// overflow.
4057
/// @return value The normalized signed fixed-point value with 18 decimals
4158
/// @return timestamp The updatedAt timestamp of the feed
4259
function read()
@@ -48,7 +65,9 @@ contract NormalizedApi3ReaderProxyV1 is INormalizedApi3ReaderProxyV1 {
4865
(, int256 answer, , uint256 updatedAt, ) = AggregatorV2V3Interface(feed)
4966
.latestRoundData();
5067

51-
value = int224(answer.scaleValue(feedDecimals, 18));
68+
value = isUpscaling
69+
? int224(answer * scalingFactor)
70+
: int224(answer / scalingFactor);
5271
timestamp = uint32(updatedAt);
5372
}
5473

contracts/ProxyUtils.sol

Lines changed: 0 additions & 25 deletions
This file was deleted.

contracts/adapters/ScaledApi3FeedProxyV1.sol

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
pragma solidity ^0.8.27;
33

44
import "@api3/contracts/interfaces/IApi3ReaderProxy.sol";
5-
import "../ProxyUtils.sol";
65
import "./interfaces/IScaledApi3FeedProxyV1.sol";
76

87
/// @title An immutable Chainlink AggregatorV2V3Interface feed contract that
@@ -11,13 +10,20 @@ import "./interfaces/IScaledApi3FeedProxyV1.sol";
1110
/// @dev This contract assumes the source proxy always returns values with
1211
/// 18 decimals (as all IApi3ReaderProxy-compatible proxies do)
1312
contract ScaledApi3FeedProxyV1 is IScaledApi3FeedProxyV1 {
14-
using ProxyUtils for int256;
15-
1613
/// @notice IApi3ReaderProxy contract address
1714
address public immutable override proxy;
1815

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

19+
/// @notice Pre-calculated factor for scaling the proxy's 18-decimal value
20+
/// to `targetDecimals`.
21+
int256 public immutable scalingFactor;
22+
23+
/// @notice True if upscaling (multiply by `scalingFactor`), false if
24+
/// downscaling (divide by `scalingFactor`), to scale to `targetDecimals`.
25+
bool public immutable isUpscaling;
26+
2127
/// @param proxy_ IApi3ReaderProxy contract address
2228
/// @param targetDecimals_ Decimals used to scale the IApi3ReaderProxy value
2329
constructor(address proxy_, uint8 targetDecimals_) {
@@ -27,9 +33,16 @@ contract ScaledApi3FeedProxyV1 is IScaledApi3FeedProxyV1 {
2733
if (targetDecimals_ == 0 || targetDecimals_ > 36) {
2834
revert InvalidDecimals();
2935
}
30-
36+
if (targetDecimals_ == 18) {
37+
revert NoScalingNeeded();
38+
}
3139
proxy = proxy_;
3240
targetDecimals = targetDecimals_;
41+
uint8 delta = targetDecimals_ > 18
42+
? targetDecimals_ - 18
43+
: 18 - targetDecimals_;
44+
scalingFactor = int256(10 ** uint256(delta));
45+
isUpscaling = targetDecimals_ > 18;
3346
}
3447

3548
/// @dev AggregatorV2V3Interface users are already responsible with
@@ -118,19 +131,26 @@ contract ScaledApi3FeedProxyV1 is IScaledApi3FeedProxyV1 {
118131
updatedAt = startedAt;
119132
}
120133

121-
/// @notice Reads from the IApi3ReaderProxy value and scales it to target
122-
/// decimals
123-
/// @dev Casting the scaled value to int224 might cause an overflow but this
124-
/// is preferable to checking the value for overflows in every read due to
125-
/// gas overhead
126-
function _read()
127-
internal
128-
view
129-
returns (int256 scaledValue, uint32 timestamp)
130-
{
131-
(int256 value, uint32 proxyTimestamp) = IApi3ReaderProxy(proxy).read();
132-
133-
scaledValue = int224(value.scaleValue(18, targetDecimals));
134+
/// @notice Reads a value from the underlying `IApi3ReaderProxy` and
135+
/// scales it to `targetDecimals`.
136+
/// @dev Reads an `int224` value (assumed to be 18 decimals) from the
137+
/// underlying `IApi3ReaderProxy`. This value is then scaled to
138+
/// `targetDecimals` using pre-calculated factors. The scaling arithmetic
139+
/// (e.g., `proxyValue * scalingFactor`) involves an `int224` (`proxyValue`)
140+
/// and an `int256` (`scalingFactor`). `proxyValue` is implicitly promoted
141+
/// to `int256` for this operation, resulting in an `int256` value.
142+
/// This allows the scaled result to exceed the `int224` range, provided
143+
/// it fits within `int256`. Arithmetic operations will revert on `int256`
144+
/// overflow. The function returns the scaled value as an `int256`.
145+
/// @return value The scaled signed fixed-point value with `targetDecimals`.
146+
/// @return timestamp The timestamp from the underlying proxy.
147+
function _read() internal view returns (int256 value, uint32 timestamp) {
148+
(int224 proxyValue, uint32 proxyTimestamp) = IApi3ReaderProxy(proxy)
149+
.read();
150+
151+
value = isUpscaling
152+
? proxyValue * scalingFactor
153+
: proxyValue / scalingFactor;
134154
timestamp = proxyTimestamp;
135155
}
136156
}

contracts/adapters/interfaces/IScaledApi3FeedProxyV1.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ 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);
16+
17+
function scalingFactor() external view returns (int256);
18+
19+
function isUpscaling() external view returns (bool);
1420
}

contracts/interfaces/INormalizedApi3ReaderProxyV1.sol

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

1313
error UnsupportedFeedDecimals();
1414

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

1719
function feed() external view returns (address feed);
20+
21+
function scalingFactor() external view returns (int256);
22+
23+
function isUpscaling() external view returns (bool);
1824
}

test/NormalizedApi3ReaderProxyV1.sol.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ describe('NormalizedApi3ReaderProxyV1', function () {
1111
return { ...acc, [roleName]: accounts[index] };
1212
}, {});
1313

14-
const decimals = 20;
15-
const answer = ethers.parseUnits('1824.97', decimals);
14+
const decimals = 8;
15+
const answer = ethers.parseUnits('0.25', decimals);
1616
const timestamp = await helpers.time.latest();
1717

1818
const mockAggregatorV2V3Factory = await ethers.getContractFactory('MockAggregatorV2V3', roles.deployer);
@@ -42,9 +42,26 @@ 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+
expect(await normalizedApi3ReaderProxyV1.isUpscaling()).to.equal(true); // 8 < 18 is true
50+
expect(await normalizedApi3ReaderProxyV1.scalingFactor()).to.equal(10_000_000_000n); // 10**(18-8)
51+
});
52+
});
53+
context('feed has 18 decimals', function () {
54+
it('reverts', async function () {
55+
const { roles, mockAggregatorV2V3Factory } = await helpers.loadFixture(deploy);
56+
const feed = await mockAggregatorV2V3Factory.deploy(18, ethers.parseEther('1'), await helpers.time.latest());
57+
const normalizedApi3ReaderProxyV1Factory = await ethers.getContractFactory(
58+
'NormalizedApi3ReaderProxyV1',
59+
roles.deployer
60+
);
61+
await expect(normalizedApi3ReaderProxyV1Factory.deploy(feed))
62+
.to.be.revertedWithCustomError(normalizedApi3ReaderProxyV1Factory, 'NoNormalizationNeeded')
63+
.withArgs();
64+
});
4865
});
4966
});
5067
context('feed is zero address', function () {

test/adapters/ScaledApi3FeedProxyV1.sol.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,22 @@ 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+
expect(await scaledApi3FeedProxyV1.isUpscaling()).to.equal(false); // targetDecimals (8) > 18 is false
96+
expect(await scaledApi3FeedProxyV1.scalingFactor()).to.equal(10_000_000_000n); // 10**(18-8)
97+
});
98+
});
99+
context('targetDecimals is 18', function () {
100+
it('reverts', async function () {
101+
const { api3ReaderProxyV1, roles } = await helpers.loadFixture(deploy);
102+
const scaledApi3FeedProxyV1 = await ethers.getContractFactory('ScaledApi3FeedProxyV1', roles.deployer);
103+
await expect(scaledApi3FeedProxyV1.deploy(await api3ReaderProxyV1.getAddress(), 18))
104+
.to.be.revertedWithCustomError(scaledApi3FeedProxyV1, 'NoScalingNeeded')
105+
.withArgs();
106+
});
94107
});
95108
});
96109
context('targetDecimals is invalid', function () {

0 commit comments

Comments
 (0)