From 030c37c6a50029f8e32529d7a008510e455dba8a Mon Sep 17 00:00:00 2001 From: ryanbajollari <54822716+rbajollari@users.noreply.github.com> Date: Tue, 14 May 2024 08:41:02 -0400 Subject: [PATCH] feat: Proxy CL interface (#40) * feat: Minimal proxy PriceFeed contract implementing Chainlink interface * Price Feed contract init * implement latestRound and getRoundData * add deployment scripts * fix deploying * fix other deployment scripts --- .env.example | 2 + contracts/pricefeed/CloneFactory.sol | 33 ++++++ contracts/pricefeed/PriceFeed.sol | 128 +++++++++++++++++++++++ package.json | 1 + scripts/createPriceFeed.ts | 37 +++++++ scripts/deployCloneFactory.ts | 36 +++++++ scripts/deployMockOjo.ts | 7 +- scripts/deployOjo.ts | 4 +- scripts/deployPriceFeedImplementation.ts | 36 +++++++ scripts/upgradeOjo.ts | 4 +- yarn.lock | 67 +++++++++++- 11 files changed, 347 insertions(+), 8 deletions(-) create mode 100644 contracts/pricefeed/CloneFactory.sol create mode 100644 contracts/pricefeed/PriceFeed.sol create mode 100644 scripts/createPriceFeed.ts create mode 100644 scripts/deployCloneFactory.ts create mode 100644 scripts/deployPriceFeedImplementation.ts diff --git a/.env.example b/.env.example index 2194556..fc1b313 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,5 @@ OJO_CHAIN=ojo OJO_ADDRESS=ojo1es9mhmnunh208ucwq8rlrl97hqulxrz8k37dcu RESOLVE_WINDOW=7200 ASSET_LIMIT=5 +PRICE_FEED_IMPLEMENTATION_CONTRACT_ADDRESS=0xD1077c12ba7C0ED41d288F5505af2Cb23bBD680a +CLONE_FACTORY_CONTRACT_ADDRESS=0x9AaE2ac2637B9f441d1537bBdCEB712854dd426B diff --git a/contracts/pricefeed/CloneFactory.sol b/contracts/pricefeed/CloneFactory.sol new file mode 100644 index 0000000..7cf565c --- /dev/null +++ b/contracts/pricefeed/CloneFactory.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./PriceFeed.sol"; +import "@openzeppelin/contracts/proxy/Clones.sol"; + +/// @title Factory for creating PriceFeed contract clones. +/// @notice This contract will create a PriceFeed clone and map its address to the clone creator. +/// @dev Cloning is done with OpenZeppelin's Clones contract. +contract CloneFactory { + event PriceFeedCloneCreated( + address _priceFeedCloneAddress + ); + + mapping (address => address) public PriceFeedCloneAddresses; + address public implementationAddress; + + /// @param _implementationAddress Address of implementation contract to be cloned. + constructor(address _implementationAddress) { + implementationAddress = _implementationAddress; + } + + /// @notice Create clone of PriceFeed contract and initialize it. + /// @dev Clone method returns address of created clone. + /// @param _priceFeedDecimals Amount of decimals a PriceFeed is denominiated in. + /// @param _priceFeedDescription Description of PriceFeed, should be set to asset symbol ticker. + function createPriceFeed(uint8 _priceFeedDecimals, string calldata _priceFeedDescription) external { + address priceFeedCloneAddress = Clones.clone(implementationAddress); + PriceFeed(priceFeedCloneAddress).initialize(_priceFeedDecimals, _priceFeedDescription); + PriceFeedCloneAddresses[msg.sender] = priceFeedCloneAddress; + emit PriceFeedCloneCreated(priceFeedCloneAddress); + } +} diff --git a/contracts/pricefeed/PriceFeed.sol b/contracts/pricefeed/PriceFeed.sol new file mode 100644 index 0000000..b0c466d --- /dev/null +++ b/contracts/pricefeed/PriceFeed.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import "../IOjo.sol"; +import "../OjoTypes.sol"; + +/// @title Contract for calling Ojo's oracle contract with chainlink's AggregatorV3Interface implemented. +/// @author Ojo Network (https://docs.ojo.network/) +contract PriceFeed is Initializable, AggregatorV3Interface { + uint8 private priceFeedDecimals; + + string private priceFeedDescription; + + IOjo public immutable ojo; + + uint80 constant DEFAULT_ROUND = 1; + + uint256 constant DEFAULT_VERSION = 1; + + uint256 internal constant INT256_MAX = uint256(type(int256).max); + + error GetRoundDataCanBeOnlyCalledWithLatestRound(uint80 requestedRoundId); + + error UnsafeUintToIntConversion(uint256 value); + + constructor(address ojo_) { + ojo = IOjo(ojo_); + } + + /// @notice Initialize clone of this contract. + /// @dev This function is used in place of a constructor in proxy contracts. + /// @param _priceFeedDecimals Amount of decimals a PriceFeed is denominiated in. + /// @param _priceFeedDescription Description of PriceFeed. + function initialize(uint8 _priceFeedDecimals, string calldata _priceFeedDescription) + external + initializer { + priceFeedDecimals = _priceFeedDecimals; + priceFeedDescription = _priceFeedDescription; + } + + /// @notice Amount of decimals price is denominated in. + function decimals() external view returns (uint8) { + return priceFeedDecimals; + } + + /// @notice Asset that this proxy is tracking. + /// @dev This should be set as the asset symbol ticker as it used to query the Ojo contract. + function description() external view returns (string memory) { + return priceFeedDescription; + } + + /// @notice Version always returns 1. + function version() external view returns (uint256) { + return DEFAULT_VERSION; + } + + /// @dev Latest round always returns 1 since this contract does not support rounds. + function latestRound() public pure returns (uint80) { + return DEFAULT_ROUND; + } + + /// @notice Fetches price data from Ojo contract from a specified round. + /// @dev Even though rounds are not utilized in this contract getRoundData is implemented for contracts + /// that still rely on it. Function will revert if specified round is not the latest round. + /// @return roundId Round ID of price data, this is always set to 1. + /// @return answer Price in USD of asset this contract is tracking. + /// @return startedAt Timestamp relating to price update. + /// @return updatedAt Timestamp relating to price update. + /// @return answeredInRound Equal to round ID. + function getRoundData(uint80 _roundId) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + if (_roundId != latestRound()) { + revert GetRoundDataCanBeOnlyCalledWithLatestRound(_roundId); + } + return latestRoundData(); + } + + /// @notice Fetches latest price data from Ojo contract. + /// @return roundId Round ID of price data, this is always set to 1. + /// @return answer Price in USD of asset this contract is tracking. + /// @return startedAt Timestamp relating to price update. + /// @return updatedAt Timestamp relating to price update. + /// @return answeredInRound Equal to round ID. + function latestRoundData() + public + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + roundId = latestRound(); + bytes32 assetName = bytes32(bytes(priceFeedDescription)); + + OjoTypes.PriceData memory priceData = ojo.getPriceData(assetName); + + if (priceData.price > INT256_MAX) { + revert UnsafeUintToIntConversion(priceData.price); + } + + // These values are equal after chainlink’s OCR update + startedAt = priceData.resolveTime; + updatedAt = priceData.resolveTime; + + // roundId is always equal to answeredInRound + answeredInRound = roundId; + + return ( + roundId, + int256(priceData.price), + startedAt, + updatedAt, + answeredInRound + ); + } +} diff --git a/package.json b/package.json index 5fe3dc9..2a08115 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "author": "rbajollari", "dependencies": { "@axelar-network/axelar-gmp-sdk-solidity": "^5.6.4", + "@chainlink/contracts": "^0.6.1", "@openzeppelin/contracts": "^5.0.0", "@openzeppelin/contracts-upgradeable": "^5.0.1", "dotenv": "^16.3.1", diff --git a/scripts/createPriceFeed.ts b/scripts/createPriceFeed.ts new file mode 100644 index 0000000..41b0ac5 --- /dev/null +++ b/scripts/createPriceFeed.ts @@ -0,0 +1,37 @@ +import { Wallet, ethers } from "ethers"; +import CloneFactory from '../artifacts/contracts/pricefeed/CloneFactory.sol/CloneFactory.json'; +import testnet_chains from '../testnet_chains.json'; +import mainnet_chains from '../mainnet_chains.json'; + +async function main() { + const cloneFactoryAddress = process.env.CLONE_FACTORY_CONTRACT_ADDRESS as string; + const priceFeedDecimals = 18; + const priceFeedDescription = "ETH"; + + const privateKey = process.env.PRIVATE_KEY; + + if (!privateKey) { + throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.'); + } + + const mainnet = process.env.MAINNET as string + let evmChains = testnet_chains.map((chain) => ({ ...chain })); + if (mainnet === "TRUE") { + evmChains = mainnet_chains.map((chain) => ({ ...chain })); + } + + for (const chain of evmChains) { + const provider = new ethers.JsonRpcProvider(chain.rpc) + const wallet = new Wallet(privateKey, provider); + const balance = await provider.getBalance(wallet.address) + console.log(`${chain.name} wallet balance: ${ethers.formatEther(balance.toString())} ${chain.tokenSymbol}`); + + const cloneFactoryContract = new ethers.Contract(cloneFactoryAddress, CloneFactory.abi, wallet) + await cloneFactoryContract.createPriceFeed(priceFeedDecimals, priceFeedDescription) + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/deployCloneFactory.ts b/scripts/deployCloneFactory.ts new file mode 100644 index 0000000..e5f73a3 --- /dev/null +++ b/scripts/deployCloneFactory.ts @@ -0,0 +1,36 @@ +import { Wallet, ethers } from "ethers"; +import CloneFactory from '../artifacts/contracts/pricefeed/CloneFactory.sol/CloneFactory.json'; +import testnet_chains from '../testnet_chains.json'; +import mainnet_chains from '../mainnet_chains.json'; + +async function main () { + const priceFeedImplementation = process.env.PRICE_FEED_IMPLEMENTATION_CONTRACT_ADDRESS; + + const privateKey = process.env.PRIVATE_KEY; + + if (!privateKey) { + throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.'); + } + + const mainnet = process.env.MAINNET as string + let evmChains = testnet_chains.map((chain) => ({ ...chain })); + if (mainnet === "TRUE") { + evmChains = mainnet_chains.map((chain) => ({ ...chain })); + } + + for (const chain of evmChains) { + const provider = new ethers.JsonRpcProvider(chain.rpc) + const wallet = new Wallet(privateKey, provider); + const balance = await provider.getBalance(wallet.address) + console.log(`${chain.name} wallet balance: ${ethers.formatEther(balance.toString())} ${chain.tokenSymbol}`); + + const priceFeedFactory = new ethers.ContractFactory(CloneFactory.abi, CloneFactory.bytecode, wallet) + const priceFeed = await priceFeedFactory.deploy(priceFeedImplementation) + console.log(`${chain.name}, address: ${await priceFeed.getAddress()}`); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/deployMockOjo.ts b/scripts/deployMockOjo.ts index 24a93a5..5db1f62 100644 --- a/scripts/deployMockOjo.ts +++ b/scripts/deployMockOjo.ts @@ -2,6 +2,7 @@ import { Wallet, ethers } from "ethers"; import MockOjo from '../artifacts/contracts/MockOjo.sol/MockOjo.json'; import Create2Deployer from '@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/deploy/Create2Deployer.sol/Create2Deployer.json'; import testnet_chains from '../testnet_chains.json'; +import mainnet_chains from '../mainnet_chains.json'; async function main() { const ojoContractddress = process.env.OJO_CONTRACT_ADDRESS; @@ -13,7 +14,11 @@ async function main() { throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.'); } - const evmChains = testnet_chains.map((chain) => ({ ...chain })); + const mainnet = process.env.MAINNET as string + let evmChains = testnet_chains.map((chain) => ({ ...chain })); + if (mainnet === "TRUE") { + evmChains = mainnet_chains.map((chain) => ({ ...chain })); + } for (const chain of evmChains) { const provider = new ethers.JsonRpcProvider(chain.rpc) diff --git a/scripts/deployOjo.ts b/scripts/deployOjo.ts index 8072fff..27af4c6 100644 --- a/scripts/deployOjo.ts +++ b/scripts/deployOjo.ts @@ -19,9 +19,9 @@ async function main() { throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.'); } - const mainnet = Boolean(process.env.MAINNET) + const mainnet = process.env.MAINNET as string let evmChains = testnet_chains.map((chain) => ({ ...chain })); - if (mainnet === true) { + if (mainnet === "TRUE") { evmChains = mainnet_chains.map((chain) => ({ ...chain })); } diff --git a/scripts/deployPriceFeedImplementation.ts b/scripts/deployPriceFeedImplementation.ts new file mode 100644 index 0000000..491f4a1 --- /dev/null +++ b/scripts/deployPriceFeedImplementation.ts @@ -0,0 +1,36 @@ +import { Wallet, ethers } from "ethers"; +import PriceFeed from '../artifacts/contracts/pricefeed/PriceFeed.sol/PriceFeed.json'; +import testnet_chains from '../testnet_chains.json'; +import mainnet_chains from '../mainnet_chains.json'; + +async function main() { + const ojoAddress = process.env.OJO_CONTRACT_ADDRESS; + + const privateKey = process.env.PRIVATE_KEY; + + if (!privateKey) { + throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.'); + } + + const mainnet = process.env.MAINNET as string + let evmChains = testnet_chains.map((chain) => ({ ...chain })); + if (mainnet === "TRUE") { + evmChains = mainnet_chains.map((chain) => ({ ...chain })); + } + + for (const chain of evmChains) { + const provider = new ethers.JsonRpcProvider(chain.rpc) + const wallet = new Wallet(privateKey, provider); + const balance = await provider.getBalance(wallet.address) + console.log(`${chain.name} wallet balance: ${ethers.formatEther(balance.toString())} ${chain.tokenSymbol}`); + + const priceFeedFactory = new ethers.ContractFactory(PriceFeed.abi, PriceFeed.bytecode, wallet) + const priceFeed = await priceFeedFactory.deploy(ojoAddress) + console.log(`${chain.name}, address: ${await priceFeed.getAddress()}`); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/upgradeOjo.ts b/scripts/upgradeOjo.ts index 6e4ce8a..b6696b1 100644 --- a/scripts/upgradeOjo.ts +++ b/scripts/upgradeOjo.ts @@ -14,9 +14,9 @@ async function main() { throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.'); } - const mainnet = Boolean(process.env.MAINNET) + const mainnet = process.env.MAINNET as string let evmChains = testnet_chains.map((chain) => ({ ...chain })); - if (mainnet === true) { + if (mainnet === "TRUE") { evmChains = mainnet_chains.map((chain) => ({ ...chain })); } diff --git a/yarn.lock b/yarn.lock index c423645..4e3fbdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,6 +34,16 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@chainlink/contracts@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@chainlink/contracts/-/contracts-0.6.1.tgz#8842b57e755793cbdbcbc45277fb5d179c993e19" + integrity sha512-EuwijGexttw0UjfrW+HygwhQIrGAbqpf1ue28R55HhWMHBzphEH0PhWm8DQmFfj5OZNy8Io66N4L0nStkZ3QKQ== + dependencies: + "@eth-optimism/contracts" "^0.5.21" + "@openzeppelin/contracts" "~4.3.3" + "@openzeppelin/contracts-upgradeable" "^4.7.3" + "@openzeppelin/contracts-v0.7" "npm:@openzeppelin/contracts@v3.4.2" + "@chainsafe/as-sha256@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz#3639df0e1435cab03f4d9870cc3ac079e57a6fc9" @@ -77,6 +87,37 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@eth-optimism/contracts@^0.5.21": + version "0.5.40" + resolved "https://registry.yarnpkg.com/@eth-optimism/contracts/-/contracts-0.5.40.tgz#d13a04a15ea947a69055e6fc74d87e215d4c936a" + integrity sha512-MrzV0nvsymfO/fursTB7m/KunkPsCndltVgfdHaT1Aj5Vi6R/doKIGGkOofHX+8B6VMZpuZosKCMQ5lQuqjt8w== + dependencies: + "@eth-optimism/core-utils" "0.12.0" + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + +"@eth-optimism/core-utils@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.12.0.tgz#6337e4599a34de23f8eceb20378de2a2de82b0ea" + integrity sha512-qW+7LZYCz7i8dRa7SRlUKIo1VBU8lvN0HeXCxJR+z+xtMzMQpPds20XJNCMclszxYQHkXY00fOT6GvFw9ZL6nw== + dependencies: + "@ethersproject/abi" "^5.7.0" + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/contracts" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/providers" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + bufio "^1.0.7" + chai "^4.3.4" + "@ethereumjs/rlp@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" @@ -179,7 +220,7 @@ dependencies: "@ethersproject/bignumber" "^5.7.0" -"@ethersproject/contracts@5.7.0": +"@ethersproject/contracts@5.7.0", "@ethersproject/contracts@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e" integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg== @@ -282,7 +323,7 @@ dependencies: "@ethersproject/logger" "^5.7.0" -"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.1", "@ethersproject/providers@^5.7.2": +"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.0", "@ethersproject/providers@^5.7.1", "@ethersproject/providers@^5.7.2": version "5.7.2" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== @@ -767,16 +808,31 @@ "@nomicfoundation/solidity-analyzer-win32-ia32-msvc" "0.1.1" "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.1" +"@openzeppelin/contracts-upgradeable@^4.7.3": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.6.tgz#38b21708a719da647de4bb0e4802ee235a0d24df" + integrity sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA== + "@openzeppelin/contracts-upgradeable@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.0.1.tgz#ebc163cbed2de6b8b69bff628261d18deb912a81" integrity sha512-MvaLoPnVcoZr/qqZP+4cl9piuR4gg0iIGgxVSZ/AL1iId3M6IdEHzz9Naw5Lirl4KKBI6ciTVnX07yL4dOMIJg== +"@openzeppelin/contracts-v0.7@npm:@openzeppelin/contracts@v3.4.2": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2.tgz#d81f786fda2871d1eb8a8c5a73e455753ba53527" + integrity sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA== + "@openzeppelin/contracts@^5.0.0": version "5.0.1" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.1.tgz#93da90fc209a0a4ff09c1deb037fbb35e4020890" integrity sha512-yQJaT5HDp9hYOOp4jTYxMsR02gdFZFXhewX5HW9Jo4fsqSVqqyIO/xTHdWDaKX5a3pv1txmf076Lziz+sO7L1w== +"@openzeppelin/contracts@~4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.3.3.tgz#ff6ee919fc2a1abaf72b22814bfb72ed129ec137" + integrity sha512-tDBopO1c98Yk7Cv/PZlHqrvtVjlgK5R4J6jxLwoO7qxK4xqOiZG+zSkIvGFpPZ0ikc3QOED3plgdqjgNTnBc7g== + "@pnpm/config.env-replace@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" @@ -1461,6 +1517,11 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +bufio@^1.0.7: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bufio/-/bufio-1.2.1.tgz#8d4ab3ddfcd5faa90f996f922f9397d41cbaf2de" + integrity sha512-9oR3zNdupcg/Ge2sSHQF3GX+kmvL/fTPvD0nd5AGLq8SjUYnTz+SlFjK/GXidndbZtIj+pVKXiWeR9w6e9wKCA== + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -1532,7 +1593,7 @@ chai-as-promised@^7.1.1: dependencies: check-error "^1.0.2" -chai@^4.2.0: +chai@^4.2.0, chai@^4.3.4: version "4.4.1" resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==