Skip to content

Commit 3503ca8

Browse files
authored
Merge pull request #14 from api3dao/13-price-cap-stable-api3readerproxyv1
Adds PriceCappedApi3ReaderProxyV1
2 parents cd4deb6 + dca1af4 commit 3503ca8

7 files changed

+575
-4
lines changed

README.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Data Feed Proxy Combinators provide modular and composable smart contracts for o
1717
- **`ScaledApi3FeedProxyV1`**: Reads from an underlying `IApi3ReaderProxy`, scales its value to a specified number of decimal places, and exposes the Chainlink `AggregatorV2V3Interface`. This makes an Api3 data feed, with adjusted precision, consumable by systems expecting a Chainlink-compatible interface with arbitrary decimals.
1818
- **`ProductApi3ReaderProxyV1`**: Takes two underlying `IApi3ReaderProxy` instances. Its `read()` method returns the product of their values, implementing the `IApi3ReaderProxy` interface.
1919
- **`NormalizedApi3ReaderProxyV1`**: Reads from an external data feed implementing the Chainlink-compatible `AggregatorV2V3Interface` and exposes the standard Api3 `IApi3ReaderProxy` interface. This allows dApps expecting an Api3 feed to consume data from other sources, useful for migration.
20+
- **`PriceCappedApi3ReaderProxyV1`**: Wraps an `IApi3ReaderProxy` to enforce price bounds. If the underlying price goes below `lowerBound` or above `upperBound`, the respective bound is returned. Implements `IPriceCappedApi3ReaderProxyV1` (thus `IApi3ReaderProxy` and `AggregatorV2V3Interface`) and includes an `isCapped()` check. Ideal for risk management, like ensuring stablecoin prices remain within a defined range or limiting exposure to extreme volatility.
2021

2122
These combinators either consume and expose the Api3 `IApi3ReaderProxy` interface or act as adapters to/from other interfaces like Chainlink's `AggregatorV2V3Interface`. This facilitates integration within the Api3 ecosystem or when bridging with other oracle systems. The output of one combinator can often serve as input for another, enabling complex data transformation pipelines.
2223

@@ -108,7 +109,7 @@ The `NETWORK` variable should be set to a chain name as defined by `@api3/contra
108109
- `FEED`: Address of the external data feed (e.g., a Chainlink `AggregatorV2V3Interface` compatible feed).
109110
- Example:
110111
```bash
111-
NETWORK=polygon-mainnet FEED=0xExternalFeedAddress pnpm deploy:NormalizedApi3ReaderProxyV1
112+
NETWORK=polygon FEED=0xExternalFeedAddress pnpm deploy:NormalizedApi3ReaderProxyV1
112113
```
113114

114115
- **`ProductApi3ReaderProxyV1`**:
@@ -118,16 +119,44 @@ The `NETWORK` variable should be set to a chain name as defined by `@api3/contra
118119
- `PROXY2`: Address of the second `IApi3ReaderProxy` contract.
119120
- Example:
120121
```bash
121-
NETWORK=arbitrum-mainnet PROXY1=0xProxy1Address PROXY2=0xProxy2Address pnpm deploy:ProductApi3ReaderProxyV1
122+
NETWORK=arbitrum PROXY1=0xProxy1Address PROXY2=0xProxy2Address pnpm deploy:ProductApi3ReaderProxyV1
122123
```
123124

124125
- **`ScaledApi3FeedProxyV1`**:
126+
125127
- `NETWORK`: Target network name.
126128
- `PROXY`: Address of the underlying `IApi3ReaderProxy` contract.
127129
- `DECIMALS`: The desired number of decimals for the scaled output.
128130
- Example:
129131
```bash
130-
NETWORK=base-mainnet PROXY=0xUnderlyingProxyAddress DECIMALS=8 pnpm deploy:ScaledApi3FeedProxyV1
132+
NETWORK=base PROXY=0xUnderlyingProxyAddress DECIMALS=8 pnpm deploy:ScaledApi3FeedProxyV1
133+
```
134+
135+
- **`PriceCappedApi3ReaderProxyV1`**:
136+
137+
- `NETWORK`: Target network name.
138+
- `PROXY`: Address of the underlying `IApi3ReaderProxy` contract.
139+
- `LOWER_BOUND`: The minimum price (inclusive) this proxy will report, as a full integer string (e.g., `"990000000000000000"` for $0.99 with 18 decimals). **Optional: Defaults to `"0"` if not provided (effectively setting only an upper bound).**
140+
- `UPPER_BOUND`: The maximum price (inclusive) this proxy will report, as a full integer string (e.g., `"1010000000000000000"` for $1.01 with 18 decimals). **Optional: Defaults to the maximum `int224` value (`(2**223 - 1)`) if not provided (effectively setting only a lower bound).** To configure a fixed price, set `UPPER_BOUND`to the same value as`LOWER_BOUND`.
141+
- Example (for a stablecoin expected to be around $1.00, with 18 decimals, capped between $0.99 and $1.01):
142+
```bash
143+
NETWORK=ethereum PROXY=0xUsdcUsdDapiAddress LOWER_BOUND="990000000000000000" UPPER_BOUND="1010000000000000000" pnpm deploy:PriceCappedApi3ReaderProxyV1
144+
```
145+
- Example (upper cap only at $1.05 for an asset, 18 decimals):
146+
```bash
147+
NETWORK=ethereum PROXY=0xAssetDapiAddress UPPER_BOUND="1050000000000000000" pnpm deploy:PriceCappedApi3ReaderProxyV1 # LOWER_BOUND defaults to "0"
148+
```
149+
- Example (fixed price at $1.00 for an asset, 18 decimals):
150+
```bash
151+
NETWORK=ethereum PROXY=0xStablecoinDapiAddress LOWER_BOUND="1000000000000000000" UPPER_BOUND="1000000000000000000" pnpm deploy:PriceCappedApi3ReaderProxyV1
152+
```
153+
- Example (lower cap only at $0.95 for an asset, 18 decimals):
154+
```bash
155+
NETWORK=ethereum PROXY=0xAssetDapiAddress LOWER_BOUND="950000000000000000" pnpm deploy:PriceCappedApi3ReaderProxyV1 # UPPER_BOUND defaults to max int224
156+
```
157+
- Example (no effective capping / pass-through, 18 decimals):
158+
```bash
159+
NETWORK=ethereum PROXY=0xAssetDapiAddress pnpm deploy:PriceCappedApi3ReaderProxyV1 # LOWER_BOUND defaults to "0" (floors negative prices), UPPER_BOUND defaults to max int224
131160
```
132161

133162
_Note: The specific `pnpm deploy:<ContractName>` scripts for each combinator are defined in the `package.json` file._

contracts/InverseApi3ReaderProxyV1.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ contract InverseApi3ReaderProxyV1 is IInverseApi3ReaderProxyV1 {
2929
/// `baseValue` is so small (yet non-zero) that the resulting inverted value
3030
/// would overflow the `int224` type.
3131
/// @return value Inverted value of the underlying proxy
32-
/// @return timestamp Timestamp from the underlying proxy
32+
/// @return timestamp Timestamp of the underlying proxy
3333
function read()
3434
public
3535
view
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.27;
3+
4+
import "@api3/contracts/interfaces/IApi3ReaderProxy.sol";
5+
import "./interfaces/IPriceCappedApi3ReaderProxyV1.sol";
6+
7+
/**
8+
* @title An immutable proxy contract that provides a price bounding mechanism.
9+
* It reads the price from the underlying Api3 proxy and if this price falls
10+
* outside a predefined `lowerBound` and `upperBound`, this contract will report
11+
* the respective bound instead.
12+
* This is primarily intended for assets (e.g., stablecoins) where a protocol
13+
* wants to limit the price range it ingests for risk management purposes.
14+
* @dev `lowerBound` and `upperBound` are immutable and set during deployment.
15+
* To set only an upper bound, `lowerBound_` can be set to 0.
16+
* To set only a lower bound, `upperBound_` can be set to `type(int224).max`.
17+
* To configure a fixed price, set `lowerBound_` and `upperBound_` to the
18+
* same desired price.
19+
* If `lowerBound_` is 0 and `upperBound_` is `type(int224).max`, no effective
20+
* capping occurs, though negative prices from the underlying proxy would be
21+
* floored at 0 if `lowerBound_` is 0.
22+
*/
23+
contract PriceCappedApi3ReaderProxyV1 is IPriceCappedApi3ReaderProxyV1 {
24+
/// @notice IApi3ReaderProxy contract address
25+
address public immutable override proxy;
26+
27+
/// @notice The minimum price (inclusive) that this proxy will report.
28+
int224 public immutable override lowerBound;
29+
30+
/// @notice The maximum price (inclusive) that this proxy will report.
31+
int224 public immutable override upperBound;
32+
33+
/// @param proxy_ IApi3ReaderProxy contract address
34+
/// @param lowerBound_ The minimum price (inclusive) this proxy will report
35+
/// @param upperBound_ The maximum price (inclusive) this proxy will report
36+
constructor(address proxy_, int224 lowerBound_, int224 upperBound_) {
37+
if (proxy_ == address(0)) {
38+
revert ZeroProxyAddress();
39+
}
40+
if (lowerBound_ < 0) {
41+
revert LowerBoundMustBeNonNegative();
42+
}
43+
if (upperBound_ < lowerBound_) {
44+
revert UpperBoundMustBeGreaterOrEqualToLowerBound();
45+
}
46+
proxy = proxy_;
47+
lowerBound = lowerBound_;
48+
upperBound = upperBound_;
49+
}
50+
51+
/// @notice Reads the current value and timestamp from the underlying
52+
/// `IApi3ReaderProxy` and applies the price bounds.
53+
/// @dev If the `baseValue` from the underlying proxy is less than
54+
/// `lowerBound`, then `lowerBound` is returned as the `value`. If
55+
/// `baseValue` is greater than `upperBound`, then `upperBound` is returned.
56+
/// Otherwise, the `baseValue` is returned. The timestamp is passed through
57+
/// unmodified.
58+
/// @return value Value of the underlying proxy, potentially bounded
59+
/// @return timestamp Timestamp of the underlying proxy
60+
function read()
61+
public
62+
view
63+
override
64+
returns (int224 value, uint32 timestamp)
65+
{
66+
(int224 baseValue, uint32 baseTimestamp) = IApi3ReaderProxy(proxy)
67+
.read();
68+
69+
timestamp = baseTimestamp;
70+
71+
if (baseValue < lowerBound) {
72+
value = lowerBound;
73+
} else if (baseValue > upperBound) {
74+
value = upperBound;
75+
} else {
76+
value = baseValue;
77+
}
78+
}
79+
80+
/// @notice Checks if the current price from the underlying proxy would be
81+
/// capped or floored by the bounds.
82+
/// @return True if the base value is less than `lowerBound` or greater
83+
/// than `upperBound`, false otherwise.
84+
function isCapped() external view returns (bool) {
85+
(int224 baseValue, ) = IApi3ReaderProxy(proxy).read();
86+
return baseValue < lowerBound || baseValue > upperBound;
87+
}
88+
89+
/// @dev AggregatorV2V3Interface users are already responsible with
90+
/// validating the values that they receive (e.g., revert if the spot price
91+
/// of an asset is negative). Therefore, this contract omits validation.
92+
function latestAnswer() external view override returns (int256 value) {
93+
(value, ) = read();
94+
}
95+
96+
/// @dev A Chainlink feed contract returns the block timestamp at which the
97+
/// feed was last updated. On the other hand, an Api3 feed timestamp
98+
/// denotes the point in time at which the first-party oracles signed the
99+
/// data used to do the last update. We find this to be a reasonable
100+
/// approximation, considering that usually the timestamp is only used to
101+
/// check if the last update is stale.
102+
function latestTimestamp()
103+
external
104+
view
105+
override
106+
returns (uint256 timestamp)
107+
{
108+
(, timestamp) = read();
109+
}
110+
111+
/// @dev Api3 feeds are updated asynchronously and not in rounds
112+
function latestRound() external pure override returns (uint256) {
113+
revert FunctionIsNotSupported();
114+
}
115+
116+
/// @dev Functions that use the round ID as an argument are not supported
117+
function getAnswer(uint256) external pure override returns (int256) {
118+
revert FunctionIsNotSupported();
119+
}
120+
121+
/// @dev Functions that use the round ID as an argument are not supported
122+
function getTimestamp(uint256) external pure override returns (uint256) {
123+
revert FunctionIsNotSupported();
124+
}
125+
126+
/// @dev Api3 feeds always use 18 decimals
127+
function decimals() external pure override returns (uint8) {
128+
return 18;
129+
}
130+
131+
/// @dev Underlying proxy dApp ID and dAPI name act as the description, and
132+
/// this is left empty to save gas on contract deployment
133+
function description() external pure override returns (string memory) {
134+
return "";
135+
}
136+
137+
/// @dev A unique version is chosen to easily check if an unverified
138+
/// contract that acts as a Chainlink feed is a PriceCappedApi3ReaderProxyV1
139+
function version() external pure override returns (uint256) {
140+
return 4918;
141+
}
142+
143+
/// @dev Functions that use the round ID as an argument are not supported
144+
function getRoundData(
145+
uint80
146+
)
147+
external
148+
pure
149+
override
150+
returns (uint80, int256, uint256, uint256, uint80)
151+
{
152+
revert FunctionIsNotSupported();
153+
}
154+
155+
/// @dev Rounds IDs are returned as `0` as invalid values.
156+
/// Similar to `latestAnswer()`, we leave the validation of the returned
157+
/// value to the caller.
158+
function latestRoundData()
159+
external
160+
view
161+
override
162+
returns (
163+
uint80 roundId,
164+
int256 answer,
165+
uint256 startedAt,
166+
uint256 updatedAt,
167+
uint80 answeredInRound
168+
)
169+
{
170+
roundId = answeredInRound = 0;
171+
(answer, startedAt) = read();
172+
updatedAt = startedAt;
173+
}
174+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.4;
3+
4+
import "@api3/contracts/interfaces/IApi3ReaderProxy.sol";
5+
import "../vendor/@chainlink/contracts@1.2.0/src/v0.8/shared/interfaces/AggregatorV2V3Interface.sol";
6+
7+
interface IPriceCappedApi3ReaderProxyV1 is
8+
IApi3ReaderProxy,
9+
AggregatorV2V3Interface
10+
{
11+
error ZeroProxyAddress();
12+
13+
error LowerBoundMustBeNonNegative();
14+
15+
error UpperBoundMustBeGreaterOrEqualToLowerBound();
16+
17+
error FunctionIsNotSupported();
18+
19+
function proxy() external view returns (address proxy);
20+
21+
function lowerBound() external view returns (int224 lowerBound);
22+
23+
function upperBound() external view returns (int224 upperBound);
24+
25+
function isCapped() external view returns (bool);
26+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { HardhatRuntimeEnvironment } from 'hardhat/types';
2+
3+
import { getDeploymentName } from '../src';
4+
5+
export const CONTRACT_NAME = 'PriceCappedApi3ReaderProxyV1';
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 lowerBound = process.env.LOWER_BOUND ? BigInt(process.env.LOWER_BOUND) : 0n; // Defaults to 0
27+
log(`Using lower bound: ${lowerBound.toString()}`);
28+
29+
const upperBound = process.env.UPPER_BOUND ? BigInt(process.env.UPPER_BOUND) : BigInt(2) ** BigInt(223) - BigInt(1); // Defaults to type(int224).max
30+
log(`Using upper bound: ${upperBound.toString()}`);
31+
32+
const isLocalNetwork = network.name === 'hardhat' || network.name === 'localhost';
33+
34+
const confirmations = isLocalNetwork ? 1 : 5;
35+
log(`Deployment confirmations: ${confirmations}`);
36+
37+
const constructorArgs = [proxyAddress, lowerBound, upperBound];
38+
const constructorArgTypes = ['address', 'int224', 'int224'];
39+
40+
const deploymentName = getDeploymentName(CONTRACT_NAME, constructorArgTypes, constructorArgs);
41+
log(`Generated deterministic deployment name for this instance: ${deploymentName}`);
42+
43+
const deployment = await deploy(deploymentName, {
44+
contract: CONTRACT_NAME,
45+
from: deployerAddress,
46+
args: constructorArgs,
47+
log: true,
48+
waitConfirmations: confirmations,
49+
});
50+
51+
if (isLocalNetwork) {
52+
log('Skipping verification on local network.');
53+
return;
54+
}
55+
56+
log(
57+
`Attempting verification of ${deploymentName} (contract type ${CONTRACT_NAME}) at ${deployment.address} (already waited for confirmations)...`
58+
);
59+
await run('verify:verify', {
60+
address: deployment.address,
61+
constructorArguments: deployment.args,
62+
});
63+
};
64+
module.exports.tags = [CONTRACT_NAME];

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"deploy:InverseApi3ReaderProxyV1": "hardhat deploy --network $NETWORK --tags InverseApi3ReaderProxyV1",
2828
"deploy:NormalizedApi3ReaderProxyV1": "hardhat deploy --network $NETWORK --tags NormalizedApi3ReaderProxyV1",
2929
"deploy:ProductApi3ReaderProxyV1": "hardhat deploy --network $NETWORK --tags ProductApi3ReaderProxyV1",
30+
"deploy:PriceCappedApi3ReaderProxyV1": "hardhat deploy --network $NETWORK --tags PriceCappedApi3ReaderProxyV1",
3031
"deploy:ScaledApi3FeedProxyV1": "hardhat deploy --network $NETWORK --tags ScaledApi3FeedProxyV1",
3132
"lint": "pnpm run prettier:check && pnpm run lint:eslint && pnpm run lint:solhint",
3233
"lint:solhint": "solhint ./contracts/**/*.sol",

0 commit comments

Comments
 (0)