Skip to content

Removes ProxyUtils library #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions contracts/NormalizedApi3ReaderProxyV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,57 @@
pragma solidity ^0.8.27;

import "./interfaces/INormalizedApi3ReaderProxyV1.sol";
import "./ProxyUtils.sol";

/// @title An immutable proxy contract that converts a Chainlink
/// AggregatorV2V3Interface feed output to 18 decimals to conform with
/// IApi3ReaderProxy decimal standard
/// IApi3ReaderProxy decimal convention
/// @dev This contract implements the AggregatorV2V3Interface to be compatible
/// with Chainlink aggregators. This allows the contract to be used as a drop-in
/// replacement for Chainlink aggregators in existing dApps.
/// Refer to https://github.com/api3dao/migrate-from-chainlink-to-api3 for more
/// information about the Chainlink interface implementation.
contract NormalizedApi3ReaderProxyV1 is INormalizedApi3ReaderProxyV1 {
using ProxyUtils for int256;

/// @notice Chainlink AggregatorV2V3Interface contract address
address public immutable override feed;

uint8 internal immutable feedDecimals;
/// @dev Pre-calculated factor for scaling to 18 decimals.
int256 private immutable scalingFactor;

/// @dev True for upscaling (multiply by scalingFactor), else downscaling
/// (divide by scalingFactor).
bool private immutable isUpscaling;

/// @param feed_ The address of the Chainlink AggregatorV2V3Interface feed
constructor(address feed_) {
if (feed_ == address(0)) {
revert ZeroProxyAddress();
}

uint8 feedDecimals_ = AggregatorV2V3Interface(feed_).decimals();
if (feedDecimals_ == 0 || feedDecimals_ > 36) {
revert UnsupportedFeedDecimals();
}
if (feedDecimals_ == 18) {
revert NoNormalizationNeeded();
}
feed = feed_;
feedDecimals = feedDecimals_;
uint8 delta = feedDecimals_ > 18
? feedDecimals_ - 18
: 18 - feedDecimals_;
scalingFactor = int256(10 ** uint256(delta));
isUpscaling = feedDecimals_ < 18;
}

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

value = int224(answer.scaleValue(feedDecimals, 18));
value = isUpscaling
? int224(answer * scalingFactor)
: int224(answer / scalingFactor);
timestamp = uint32(updatedAt);
}

Expand Down
25 changes: 0 additions & 25 deletions contracts/ProxyUtils.sol

This file was deleted.

53 changes: 36 additions & 17 deletions contracts/adapters/ScaledApi3FeedProxyV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
pragma solidity ^0.8.27;

import "@api3/contracts/interfaces/IApi3ReaderProxy.sol";
import "../ProxyUtils.sol";
import "./interfaces/IScaledApi3FeedProxyV1.sol";

/// @title An immutable Chainlink AggregatorV2V3Interface feed contract that
Expand All @@ -11,13 +10,19 @@ import "./interfaces/IScaledApi3FeedProxyV1.sol";
/// @dev This contract assumes the source proxy always returns values with
/// 18 decimals (as all IApi3ReaderProxy-compatible proxies do)
contract ScaledApi3FeedProxyV1 is IScaledApi3FeedProxyV1 {
using ProxyUtils for int256;

/// @notice IApi3ReaderProxy contract address
address public immutable override proxy;

/// @dev Target decimals for the scaled value.
uint8 private immutable targetDecimals;

/// @dev Pre-calculated factor for scaling from 18 decimals.
int256 private immutable scalingFactor;

/// @dev True for upscaling (multiply by scalingFactor), else downscaling
/// (divide by scalingFactor).
bool private immutable isUpscaling;

/// @param proxy_ IApi3ReaderProxy contract address
/// @param targetDecimals_ Decimals used to scale the IApi3ReaderProxy value
constructor(address proxy_, uint8 targetDecimals_) {
Expand All @@ -27,9 +32,16 @@ contract ScaledApi3FeedProxyV1 is IScaledApi3FeedProxyV1 {
if (targetDecimals_ == 0 || targetDecimals_ > 36) {
revert InvalidDecimals();
}

if (targetDecimals_ == 18) {
revert NoScalingNeeded();
}
proxy = proxy_;
targetDecimals = targetDecimals_;
uint8 delta = targetDecimals_ > 18
? targetDecimals_ - 18
: 18 - targetDecimals_;
scalingFactor = int256(10 ** uint256(delta));
isUpscaling = targetDecimals_ > 18;
}

/// @dev AggregatorV2V3Interface users are already responsible with
Expand Down Expand Up @@ -118,19 +130,26 @@ contract ScaledApi3FeedProxyV1 is IScaledApi3FeedProxyV1 {
updatedAt = startedAt;
}

/// @notice Reads from the IApi3ReaderProxy value and scales it to target
/// decimals
/// @dev Casting the scaled value to int224 might cause an overflow but this
/// is preferable to checking the value for overflows in every read due to
/// gas overhead
function _read()
internal
view
returns (int256 scaledValue, uint32 timestamp)
{
(int256 value, uint32 proxyTimestamp) = IApi3ReaderProxy(proxy).read();

scaledValue = int224(value.scaleValue(18, targetDecimals));
/// @notice Reads a value from the underlying `IApi3ReaderProxy` and
/// scales it to `targetDecimals`.
/// @dev Reads an `int224` value (assumed to be 18 decimals) from the
/// underlying `IApi3ReaderProxy`. This value is then scaled to
/// `targetDecimals` using pre-calculated factors. The scaling arithmetic
/// (e.g., `proxyValue * scalingFactor`) involves an `int224` (`proxyValue`)
/// and an `int256` (`scalingFactor`). `proxyValue` is implicitly promoted
/// to `int256` for this operation, resulting in an `int256` value.
/// This allows the scaled result to exceed the `int224` range, provided
/// it fits within `int256`. Arithmetic operations will revert on `int256`
/// overflow. The function returns the scaled value as an `int256`.
/// @return value The scaled signed fixed-point value with `targetDecimals`.
/// @return timestamp The timestamp from the underlying proxy.
function _read() internal view returns (int256 value, uint32 timestamp) {
(int224 proxyValue, uint32 proxyTimestamp) = IApi3ReaderProxy(proxy)
.read();

value = isUpscaling
? proxyValue * scalingFactor
: proxyValue / scalingFactor;
timestamp = proxyTimestamp;
}
}
2 changes: 2 additions & 0 deletions contracts/adapters/interfaces/IScaledApi3FeedProxyV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ interface IScaledApi3FeedProxyV1 is AggregatorV2V3Interface {

error InvalidDecimals();

error NoScalingNeeded();

error FunctionIsNotSupported();

function proxy() external view returns (address proxy);
Expand Down
2 changes: 2 additions & 0 deletions contracts/interfaces/INormalizedApi3ReaderProxyV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface INormalizedApi3ReaderProxyV1 is

error UnsupportedFeedDecimals();

error NoNormalizationNeeded();

error FunctionIsNotSupported();

function feed() external view returns (address feed);
Expand Down
21 changes: 18 additions & 3 deletions test/NormalizedApi3ReaderProxyV1.sol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,24 @@ describe('NormalizedApi3ReaderProxyV1', function () {

describe('constructor', function () {
context('feed is not zero address', function () {
it('constructs', async function () {
const { feed, normalizedApi3ReaderProxyV1 } = await helpers.loadFixture(deploy);
expect(await normalizedApi3ReaderProxyV1.feed()).to.equal(await feed.getAddress());
context('feed does not have 18 decimals', function () {
it('constructs', async function () {
const { feed, normalizedApi3ReaderProxyV1 } = await helpers.loadFixture(deploy);
expect(await normalizedApi3ReaderProxyV1.feed()).to.equal(await feed.getAddress());
});
});
context('feed has 18 decimals', function () {
it('reverts', async function () {
const { roles, mockAggregatorV2V3Factory } = await helpers.loadFixture(deploy);
const feed = await mockAggregatorV2V3Factory.deploy(18, ethers.parseEther('1'), await helpers.time.latest());
const normalizedApi3ReaderProxyV1Factory = await ethers.getContractFactory(
'NormalizedApi3ReaderProxyV1',
roles.deployer
);
await expect(normalizedApi3ReaderProxyV1Factory.deploy(feed))
.to.be.revertedWithCustomError(normalizedApi3ReaderProxyV1Factory, 'NoNormalizationNeeded')
.withArgs();
});
});
});
context('feed is zero address', function () {
Expand Down
17 changes: 14 additions & 3 deletions test/adapters/ScaledApi3FeedProxyV1.sol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,20 @@ describe('ScaledApi3FeedProxyV1', function () {
describe('constructor', function () {
context('proxy is not zero address', function () {
context('targetDecimals is not invalid', function () {
it('constructs', async function () {
const { api3ReaderProxyV1, scaledApi3FeedProxyV1 } = await helpers.loadFixture(deploy);
expect(await scaledApi3FeedProxyV1.proxy()).to.equal(await api3ReaderProxyV1.getAddress());
context('targetDecimals is not 18', function () {
it('constructs', async function () {
const { api3ReaderProxyV1, scaledApi3FeedProxyV1 } = await helpers.loadFixture(deploy);
expect(await scaledApi3FeedProxyV1.proxy()).to.equal(await api3ReaderProxyV1.getAddress());
});
});
context('targetDecimals is 18', function () {
it('reverts', async function () {
const { api3ReaderProxyV1, roles } = await helpers.loadFixture(deploy);
const scaledApi3FeedProxyV1 = await ethers.getContractFactory('ScaledApi3FeedProxyV1', roles.deployer);
await expect(scaledApi3FeedProxyV1.deploy(await api3ReaderProxyV1.getAddress(), 18))
.to.be.revertedWithCustomError(scaledApi3FeedProxyV1, 'NoScalingNeeded')
.withArgs();
});
});
});
context('targetDecimals is invalid', function () {
Expand Down