-
Notifications
You must be signed in to change notification settings - Fork 0
Adds PriceCappedApi3ReaderProxyV1 #14
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3127a39
Adds PriceCapStableApi3ReaderProxyV1
acenolaza 579ac4b
Renames the contract and adds lower/upper bound for capping
acenolaza 37c266b
Allows fixed-price configuration, updates README.md and adds deploy s…
acenolaza 56c92f7
Minor wording change
acenolaza dca1af4
Adds PriceCapStableApi3ReaderProxyV1 unit tests
acenolaza File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.27; | ||
|
||
import "@api3/contracts/interfaces/IApi3ReaderProxy.sol"; | ||
import "./interfaces/IPriceCappedApi3ReaderProxyV1.sol"; | ||
|
||
/** | ||
* @title An immutable proxy contract that provides a price bounding mechanism. | ||
* It reads the price from the underlying Api3 proxy and if this price falls | ||
* outside a predefined `lowerBound` and `upperBound`, this contract will report | ||
* the respective bound instead. | ||
* This is primarily intended for assets (e.g., stablecoins) where a protocol | ||
* wants to limit the price range it ingests for risk management purposes. | ||
* @dev `lowerBound` and `upperBound` are immutable and set during deployment. | ||
* To set only an upper bound, `lowerBound_` can be set to 0. | ||
* To set only a lower bound, `upperBound_` can be set to `type(int224).max`. | ||
* To configure a fixed price, set `lowerBound_` and `upperBound_` to the | ||
* same desired price. | ||
* If `lowerBound_` is 0 and `upperBound_` is `type(int224).max`, no effective | ||
* capping occurs, though negative prices from the underlying proxy would be | ||
* floored at 0 if `lowerBound_` is 0. | ||
*/ | ||
contract PriceCappedApi3ReaderProxyV1 is IPriceCappedApi3ReaderProxyV1 { | ||
/// @notice IApi3ReaderProxy contract address | ||
address public immutable override proxy; | ||
|
||
/// @notice The minimum price (inclusive) that this proxy will report. | ||
int224 public immutable override lowerBound; | ||
|
||
/// @notice The maximum price (inclusive) that this proxy will report. | ||
int224 public immutable override upperBound; | ||
|
||
/// @param proxy_ IApi3ReaderProxy contract address | ||
/// @param lowerBound_ The minimum price (inclusive) this proxy will report | ||
/// @param upperBound_ The maximum price (inclusive) this proxy will report | ||
constructor(address proxy_, int224 lowerBound_, int224 upperBound_) { | ||
if (proxy_ == address(0)) { | ||
revert ZeroProxyAddress(); | ||
} | ||
if (lowerBound_ < 0) { | ||
revert LowerBoundMustBeNonNegative(); | ||
} | ||
if (upperBound_ < lowerBound_) { | ||
revert UpperBoundMustBeGreaterOrEqualToLowerBound(); | ||
} | ||
proxy = proxy_; | ||
lowerBound = lowerBound_; | ||
upperBound = upperBound_; | ||
} | ||
|
||
/// @notice Reads the current value and timestamp from the underlying | ||
/// `IApi3ReaderProxy` and applies the price bounds. | ||
/// @dev If the `baseValue` from the underlying proxy is less than | ||
/// `lowerBound`, then `lowerBound` is returned as the `value`. If | ||
/// `baseValue` is greater than `upperBound`, then `upperBound` is returned. | ||
/// Otherwise, the `baseValue` is returned. The timestamp is passed through | ||
/// unmodified. | ||
/// @return value Value of the underlying proxy, potentially bounded | ||
/// @return timestamp Timestamp of the underlying proxy | ||
function read() | ||
public | ||
view | ||
override | ||
returns (int224 value, uint32 timestamp) | ||
{ | ||
(int224 baseValue, uint32 baseTimestamp) = IApi3ReaderProxy(proxy) | ||
.read(); | ||
|
||
timestamp = baseTimestamp; | ||
|
||
if (baseValue < lowerBound) { | ||
value = lowerBound; | ||
} else if (baseValue > upperBound) { | ||
value = upperBound; | ||
} else { | ||
value = baseValue; | ||
} | ||
} | ||
|
||
/// @notice Checks if the current price from the underlying proxy would be | ||
/// capped or floored by the bounds. | ||
/// @return True if the base value is less than `lowerBound` or greater | ||
/// than `upperBound`, false otherwise. | ||
function isCapped() external view returns (bool) { | ||
(int224 baseValue, ) = IApi3ReaderProxy(proxy).read(); | ||
return baseValue < lowerBound || baseValue > upperBound; | ||
} | ||
|
||
/// @dev AggregatorV2V3Interface users are already responsible with | ||
/// validating the values that they receive (e.g., revert if the spot price | ||
/// of an asset is negative). Therefore, this contract omits validation. | ||
function latestAnswer() external view override returns (int256 value) { | ||
(value, ) = read(); | ||
} | ||
|
||
/// @dev A Chainlink feed contract returns the block timestamp at which the | ||
/// feed was last updated. On the other hand, an Api3 feed timestamp | ||
/// denotes the point in time at which the first-party oracles signed the | ||
/// data used to do the last update. We find this to be a reasonable | ||
/// approximation, considering that usually the timestamp is only used to | ||
/// check if the last update is stale. | ||
function latestTimestamp() | ||
external | ||
view | ||
override | ||
returns (uint256 timestamp) | ||
{ | ||
(, timestamp) = read(); | ||
} | ||
|
||
/// @dev Api3 feeds are updated asynchronously and not in rounds | ||
function latestRound() external pure override returns (uint256) { | ||
revert FunctionIsNotSupported(); | ||
} | ||
|
||
/// @dev Functions that use the round ID as an argument are not supported | ||
function getAnswer(uint256) external pure override returns (int256) { | ||
revert FunctionIsNotSupported(); | ||
} | ||
|
||
/// @dev Functions that use the round ID as an argument are not supported | ||
function getTimestamp(uint256) external pure override returns (uint256) { | ||
revert FunctionIsNotSupported(); | ||
} | ||
|
||
/// @dev Api3 feeds always use 18 decimals | ||
function decimals() external pure override returns (uint8) { | ||
return 18; | ||
} | ||
|
||
/// @dev Underlying proxy dApp ID and dAPI name act as the description, and | ||
/// this is left empty to save gas on contract deployment | ||
function description() external pure override returns (string memory) { | ||
return ""; | ||
} | ||
|
||
/// @dev A unique version is chosen to easily check if an unverified | ||
/// contract that acts as a Chainlink feed is a PriceCappedApi3ReaderProxyV1 | ||
function version() external pure override returns (uint256) { | ||
return 4918; | ||
} | ||
|
||
/// @dev Functions that use the round ID as an argument are not supported | ||
function getRoundData( | ||
uint80 | ||
) | ||
external | ||
pure | ||
override | ||
returns (uint80, int256, uint256, uint256, uint80) | ||
{ | ||
revert FunctionIsNotSupported(); | ||
} | ||
|
||
/// @dev Rounds IDs are returned as `0` as invalid values. | ||
/// Similar to `latestAnswer()`, we leave the validation of the returned | ||
/// value to the caller. | ||
function latestRoundData() | ||
external | ||
view | ||
override | ||
returns ( | ||
uint80 roundId, | ||
int256 answer, | ||
uint256 startedAt, | ||
uint256 updatedAt, | ||
uint80 answeredInRound | ||
) | ||
{ | ||
roundId = answeredInRound = 0; | ||
(answer, startedAt) = read(); | ||
updatedAt = startedAt; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.4; | ||
|
||
import "@api3/contracts/interfaces/IApi3ReaderProxy.sol"; | ||
import "../vendor/@chainlink/contracts@1.2.0/src/v0.8/shared/interfaces/AggregatorV2V3Interface.sol"; | ||
|
||
interface IPriceCappedApi3ReaderProxyV1 is | ||
IApi3ReaderProxy, | ||
AggregatorV2V3Interface | ||
{ | ||
error ZeroProxyAddress(); | ||
|
||
error LowerBoundMustBeNonNegative(); | ||
|
||
error UpperBoundMustBeGreaterOrEqualToLowerBound(); | ||
|
||
error FunctionIsNotSupported(); | ||
|
||
function proxy() external view returns (address proxy); | ||
|
||
function lowerBound() external view returns (int224 lowerBound); | ||
|
||
function upperBound() external view returns (int224 upperBound); | ||
|
||
function isCapped() external view returns (bool); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import type { HardhatRuntimeEnvironment } from 'hardhat/types'; | ||
|
||
import { getDeploymentName } from '../src'; | ||
|
||
export const CONTRACT_NAME = 'PriceCappedApi3ReaderProxyV1'; | ||
|
||
module.exports = async (hre: HardhatRuntimeEnvironment) => { | ||
const { getUnnamedAccounts, deployments, ethers, network, run } = hre; | ||
const { deploy, log } = deployments; | ||
|
||
const [deployerAddress] = await getUnnamedAccounts(); | ||
if (!deployerAddress) { | ||
throw new Error('No deployer address found.'); | ||
} | ||
log(`Deployer address: ${deployerAddress}`); | ||
|
||
const proxyAddress = process.env.PROXY; | ||
if (!proxyAddress) { | ||
throw new Error('PROXY environment variable not set. Please provide the address of the proxy contract.'); | ||
} | ||
if (!ethers.isAddress(proxyAddress)) { | ||
throw new Error(`Invalid address provided for PROXY: ${proxyAddress}`); | ||
} | ||
log(`Proxy address: ${proxyAddress}`); | ||
|
||
const lowerBound = process.env.LOWER_BOUND ? BigInt(process.env.LOWER_BOUND) : 0n; // Defaults to 0 | ||
log(`Using lower bound: ${lowerBound.toString()}`); | ||
|
||
const upperBound = process.env.UPPER_BOUND ? BigInt(process.env.UPPER_BOUND) : BigInt(2) ** BigInt(223) - BigInt(1); // Defaults to type(int224).max | ||
log(`Using upper bound: ${upperBound.toString()}`); | ||
|
||
const isLocalNetwork = network.name === 'hardhat' || network.name === 'localhost'; | ||
|
||
const confirmations = isLocalNetwork ? 1 : 5; | ||
log(`Deployment confirmations: ${confirmations}`); | ||
|
||
const constructorArgs = [proxyAddress, lowerBound, upperBound]; | ||
const constructorArgTypes = ['address', 'int224', 'int224']; | ||
|
||
const deploymentName = getDeploymentName(CONTRACT_NAME, constructorArgTypes, constructorArgs); | ||
log(`Generated deterministic deployment name for this instance: ${deploymentName}`); | ||
|
||
const deployment = await deploy(deploymentName, { | ||
contract: CONTRACT_NAME, | ||
from: deployerAddress, | ||
args: constructorArgs, | ||
log: true, | ||
waitConfirmations: confirmations, | ||
}); | ||
|
||
if (isLocalNetwork) { | ||
log('Skipping verification on local network.'); | ||
return; | ||
} | ||
|
||
log( | ||
`Attempting verification of ${deploymentName} (contract type ${CONTRACT_NAME}) at ${deployment.address} (already waited for confirmations)...` | ||
); | ||
await run('verify:verify', { | ||
address: deployment.address, | ||
constructorArguments: deployment.args, | ||
}); | ||
}; | ||
module.exports.tags = [CONTRACT_NAME]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up not checking the bounds against the current proxy value for 2 reasons:
deployer
to provide a valid proxy value