Skip to content

Commit d326fd9

Browse files
committed
Merge remote-tracking branch 'origin/proxy-deploy-scripts' into docs
2 parents 40b29f2 + 1b58070 commit d326fd9

29 files changed

+1124
-268
lines changed

.eslintrc.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
module.exports = {
22
extends: ['plugin:@api3/eslint-plugin-commons/universal', 'plugin:@api3/eslint-plugin-commons/jest'],
33
parserOptions: {
4-
project: ['./tsconfig.json'],
4+
project: './tsconfig.json',
5+
tsconfigRootDir: __dirname,
56
},
67
rules: {
78
camelcase: 'off',
@@ -26,4 +27,5 @@ module.exports = {
2627
'@typescript-eslint/no-unsafe-call': 'off',
2728
'@typescript-eslint/require-await': 'off',
2829
},
30+
ignorePatterns: ['typechain-types/*'],
2931
};

.husky/pre-push

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pnpm check:deployment-addresses

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/WstETHApi3ReaderProxyV1.sol

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.27;
3+
4+
import "@api3/contracts/interfaces/IApi3ReaderProxy.sol";
5+
import "./interfaces/IWstETH.sol";
6+
7+
/// @title An immutable proxy contract that reads the stETH per wstETH ratio
8+
/// directly from the WstETH contract on Ethereum.
9+
/// @dev This contract implements only the IApi3ReaderProxy interface and not the
10+
/// AggregatorV2V3Interface which is usually implemented by Api3 proxies. The
11+
/// user of this contract needs to be aware of this limitation and only use this
12+
/// contract where the IApi3ReaderProxy interface is expected.
13+
contract WstETHApi3ReaderProxyV1 is IApi3ReaderProxy {
14+
/// @notice The address of the wstETH contract on Ethereum mainnet.
15+
address public constant WST_ETH =
16+
0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
17+
18+
/// @inheritdoc IApi3ReaderProxy
19+
/// @dev Returns the stETH/wstETH exchange rate with 18 decimals precision.
20+
/// The timestamp returned is the current block timestamp.
21+
function read()
22+
public
23+
view
24+
override
25+
returns (int224 value, uint32 timestamp)
26+
{
27+
uint256 stEthPerToken = IWstETH(WST_ETH).stEthPerToken();
28+
29+
value = int224(int256(stEthPerToken)); // stEthPerToken value has 18 decimals.
30+
timestamp = uint32(block.timestamp);
31+
}
32+
}

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
}

contracts/interfaces/IWstETH.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.27;
3+
4+
/// @title A minimal interface for the wstETH contract on Ethereum
5+
/// @dev This interface only includes the stEthPerToken function needed to read
6+
/// the exchange rate between stETH and wstETH.
7+
interface IWstETH {
8+
/// @notice Returns the amount of stETH that corresponds to 1 wstETH
9+
/// @return The stETH/wstETH exchange rate with 18 decimals precision
10+
function stEthPerToken() external view returns (uint256);
11+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { HardhatRuntimeEnvironment } from 'hardhat/types';
2+
3+
import { getDeploymentName } from '../src';
4+
5+
export const CONTRACT_NAME = 'InverseApi3ReaderProxyV1';
6+
7+
module.exports = async (hre: HardhatRuntimeEnvironment) => {
8+
const { getUnnamedAccounts, deployments, ethers, network, run } = hre;
9+
const { deploy, log } = deployments;
10+
11+
const [deployerAddress] = await getUnnamedAccounts();
12+
if (!deployerAddress) {
13+
throw new Error('No deployer address found.');
14+
}
15+
log(`Deployer address: ${deployerAddress}`);
16+
17+
const proxyAddress = process.env.PROXY;
18+
if (!proxyAddress) {
19+
throw new Error('PROXY environment variable not set. Please provide the address of the proxy contract.');
20+
}
21+
if (!ethers.isAddress(proxyAddress)) {
22+
throw new Error(`Invalid address provided for PROXY: ${proxyAddress}`);
23+
}
24+
log(`Proxy address: ${proxyAddress}`);
25+
26+
const isLocalNetwork = network.name === 'hardhat' || network.name === 'localhost';
27+
28+
const confirmations = isLocalNetwork ? 1 : 5;
29+
log(`Deployment confirmations: ${confirmations}`);
30+
31+
const constructorArgs = [proxyAddress];
32+
const constructorArgTypes = ['address'];
33+
34+
const deploymentName = getDeploymentName(CONTRACT_NAME, constructorArgTypes, constructorArgs);
35+
log(`Generated deterministic deployment name for this instance: ${deploymentName}`);
36+
37+
const deployment = await deploy(deploymentName, {
38+
contract: CONTRACT_NAME,
39+
from: deployerAddress,
40+
args: constructorArgs,
41+
log: true,
42+
waitConfirmations: confirmations,
43+
});
44+
45+
if (isLocalNetwork) {
46+
log('Skipping verification on local network.');
47+
return;
48+
}
49+
50+
log(
51+
`Attempting verification of ${deploymentName} (contract type ${CONTRACT_NAME}) at ${deployment.address} (already waited for confirmations)...`
52+
);
53+
await run('verify:verify', {
54+
address: deployment.address,
55+
constructorArguments: deployment.args,
56+
});
57+
};
58+
module.exports.tags = [CONTRACT_NAME];

0 commit comments

Comments
 (0)