diff --git a/contracts/mock/oracles/uniswap-v2/UniswapV2FactoryMock.sol b/contracts/mock/oracles/uniswap-v2/UniswapV2FactoryMock.sol new file mode 100644 index 00000000..68be6692 --- /dev/null +++ b/contracts/mock/oracles/uniswap-v2/UniswapV2FactoryMock.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {IUniswapV2Pair} from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; + +import {UniswapV2PairMock} from "./UniswapV2PairMock.sol"; + +contract UniswapV2FactoryMock { + mapping(address => mapping(address => address)) public getPair; + address[] public allPairs; + + function createPair(address tokenA, address tokenB) external returns (address pair) { + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + + bytes memory bytecode = type(UniswapV2PairMock).creationCode; + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + + assembly { + pair := create2(0, add(bytecode, 32), mload(bytecode), salt) + } + + IUniswapV2Pair(pair).initialize(token0, token1); + + getPair[token0][token1] = pair; + getPair[token1][token0] = pair; + + allPairs.push(pair); + } +} diff --git a/contracts/mock/oracles/uniswap-v2/UniswapV2OracleMock.sol b/contracts/mock/oracles/uniswap-v2/UniswapV2OracleMock.sol new file mode 100644 index 00000000..c32cd474 --- /dev/null +++ b/contracts/mock/oracles/uniswap-v2/UniswapV2OracleMock.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {UniswapV2Oracle} from "../../../oracles/UniswapV2Oracle.sol"; +import {UniswapV2PairMock} from "./UniswapV2PairMock.sol"; + +contract UniswapV2OracleMock is UniswapV2Oracle { + using EnumerableSet for EnumerableSet.AddressSet; + + function __OracleV2Mock_init( + address uniswapV2Factory_, + uint256 timeWindow_ + ) external initializer { + __OracleV2_init(uniswapV2Factory_, timeWindow_); + } + + function mockInit(address uniswapV2Factory_, uint256 timeWindow_) external { + __OracleV2_init(uniswapV2Factory_, timeWindow_); + } + + function addPaths(address[][] calldata paths_) external { + _addPaths(paths_); + } + + function removePaths(address[] calldata tokenIns_) external { + _removePaths(tokenIns_); + } + + function setTimeWindow(uint256 newTimeWindow_) external { + _setTimeWindow(newTimeWindow_); + } + + function doubleUpdatePrice() external { + updatePrices(); + updatePrices(); + } +} diff --git a/contracts/mock/oracles/uniswap-v2/UniswapV2PairMock.sol b/contracts/mock/oracles/uniswap-v2/UniswapV2PairMock.sol new file mode 100644 index 00000000..e21cb2bf --- /dev/null +++ b/contracts/mock/oracles/uniswap-v2/UniswapV2PairMock.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +contract UniswapV2PairMock { + address public token0; + address public token1; + + uint256 public price0CumulativeLast; + uint256 public price1CumulativeLast; + + uint112 private _reserve0; + uint112 private _reserve1; + uint32 private _blockTimestampLast; + + function initialize(address token0_, address token1_) external { + token0 = token0_; + token1 = token1_; + + _reserve0 = 1 ether; + _reserve1 = 1 ether; + _blockTimestampLast = uint32(block.timestamp); + } + + function swap(uint256 amount0Out_, uint256 amount1Out_) external { + unchecked { + uint32 blockTimestamp_ = uint32(block.timestamp); + uint32 timeElapsed_ = blockTimestamp_ - _blockTimestampLast; // overflow is desired + + if (timeElapsed_ > 0 && _reserve0 != 0 && _reserve1 != 0) { + price0CumulativeLast += ((uint256(_reserve1) << 112) / (_reserve0)) * timeElapsed_; + price1CumulativeLast += ((uint256(_reserve0) << 112) / (_reserve1)) * timeElapsed_; + } + + _reserve0 = uint112(_reserve0 - amount0Out_); + _reserve1 = uint112(_reserve1 - amount1Out_); + _blockTimestampLast = blockTimestamp_; + } + } + + function getReserves() + external + view + returns (uint112 reserve0_, uint112 reserve1_, uint32 blockTimestampLast_) + { + reserve0_ = _reserve0; + reserve1_ = _reserve1; + blockTimestampLast_ = _blockTimestampLast; + } +} diff --git a/contracts/oracles/UniswapV2Oracle.sol b/contracts/oracles/UniswapV2Oracle.sol new file mode 100644 index 00000000..be56dcc9 --- /dev/null +++ b/contracts/oracles/UniswapV2Oracle.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import {IUniswapV2Factory} from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol"; +import {IUniswapV2Pair} from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; +import {UniswapV2OracleLibrary} from "@uniswap/v2-periphery/contracts/libraries/UniswapV2OracleLibrary.sol"; + +import {ArrayHelper} from "../libs/arrays/ArrayHelper.sol"; + +/** + * @notice UniswapV2Oracle module + * + * A contract for retrieving prices from Uniswap V2 pairs. Works by keeping track of pairs that were + * added as paths and returns prices of tokens following the configured routes. + * + * Arbitrary time window (time between oracle observations) may be configured and the Oracle will adjust automatically. + * + * From time to time `updatePrices()` function has to be called in order to calculate correct TWAP. + */ +abstract contract UniswapV2Oracle is Initializable { + using EnumerableSet for EnumerableSet.AddressSet; + using UniswapV2OracleLibrary for address; + using ArrayHelper for uint256[]; + using Math for uint256; + + struct PairInfo { + uint256[] prices0Cumulative; + uint256[] prices1Cumulative; + uint256[] blockTimestamps; + uint256 refs; + } + + IUniswapV2Factory public uniswapV2Factory; + + uint256 public timeWindow; + + EnumerableSet.AddressSet private _pairs; + mapping(address => address[]) private _paths; + mapping(address => PairInfo) private _pairInfos; + + /** + * @notice Constructor + * @param uniswapV2Factory_ the Uniswap V2 factory + * @param timeWindow_ the time between oracle observations + */ + function __OracleV2_init( + address uniswapV2Factory_, + uint256 timeWindow_ + ) internal onlyInitializing { + uniswapV2Factory = IUniswapV2Factory(uniswapV2Factory_); + + _setTimeWindow(timeWindow_); + } + + /** + * @notice Updates the price data for all the registered Uniswap V2 pairs + * + * May be called at any time. The time window automatically adjusts + */ + function updatePrices() public virtual { + uint256 pairsLength_ = _pairs.length(); + + for (uint256 i = 0; i < pairsLength_; i++) { + address pair_ = _pairs.at(i); + + PairInfo storage pairInfo = _pairInfos[pair_]; + uint256[] storage pairTimestamps = pairInfo.blockTimestamps; + + (uint256 price0Cumulative_, uint256 price1Cumulative_, uint256 blockTimestamp_) = pair_ + .currentCumulativePrices(); + + if ( + pairTimestamps.length == 0 || + blockTimestamp_ > pairTimestamps[pairTimestamps.length - 1] + ) { + pairInfo.prices0Cumulative.push(price0Cumulative_); + pairInfo.prices1Cumulative.push(price1Cumulative_); + pairInfo.blockTimestamps.push(blockTimestamp_); + } + } + } + + /** + * @notice The function to retrieve the price of a token following the configured route + * @param tokenIn_ The input token address + * @param amount_ The amount of the input token + * @return The price in the last token of the route + * @return The output token address + */ + function getPrice(address tokenIn_, uint256 amount_) public view returns (uint256, address) { + address[] storage path = _paths[tokenIn_]; + uint256 pathLength_ = path.length; + + require(pathLength_ > 1, "UniswapV2Oracle: invalid path"); + + address tokenOut_ = path[pathLength_ - 1]; + + for (uint256 i = 0; i < pathLength_ - 1; i++) { + (address currentToken_, address nextToken_) = (path[i], path[i + 1]); + address pair_ = uniswapV2Factory.getPair(currentToken_, nextToken_); + uint256 price_ = _getPrice(pair_, currentToken_); + + amount_ = price_.mulDiv(amount_, 2 ** 112); + } + + return (amount_, tokenOut_); + } + + /** + * @notice The function to get the route of the token + * @param tokenIn_ the token to get the route of + * @return the route of the provided token + */ + function getPath(address tokenIn_) public view returns (address[] memory) { + return _paths[tokenIn_]; + } + + /** + * @notice The function to get all the pairs the oracle tracks + * @return the array of pairs + */ + function getPairs() public view returns (address[] memory) { + return _pairs.values(); + } + + /** + * @notice The function to get the number of observations of a pair + * @param pair_ the pair address + * @return the number of oracle observations + */ + function getPairRounds(address pair_) public view returns (uint256) { + return _pairInfos[pair_].blockTimestamps.length; + } + + /** + * @notice The function to get the exact observation of a pair + * @param pair_ the pair address + * @param round_ the observation index + * @return the prices0Cumulative of the observation + * @return the prices1Cumulative of the observation + * @return the timestamp of the observation + */ + function getPairInfo( + address pair_, + uint256 round_ + ) public view returns (uint256, uint256, uint256) { + PairInfo storage _pairInfo = _pairInfos[pair_]; + + return ( + _pairInfo.prices0Cumulative[round_], + _pairInfo.prices1Cumulative[round_], + _pairInfo.blockTimestamps[round_] + ); + } + + /** + * @notice The function to set the time window of TWAP + * @param newTimeWindow_ the new time window value in seconds + */ + function _setTimeWindow(uint256 newTimeWindow_) internal { + require(newTimeWindow_ > 0, "UniswapV2Oracle: time window can't be 0"); + + timeWindow = newTimeWindow_; + } + + /** + * @notice The function to add multiple tokens paths for the oracle to observe. Every token may only have a single path + * @param paths_ the array of token paths to add + */ + function _addPaths(address[][] memory paths_) internal { + uint256 numberOfPaths_ = paths_.length; + + for (uint256 i = 0; i < numberOfPaths_; i++) { + uint256 pathLength_ = paths_[i].length; + + require(pathLength_ >= 2, "UniswapV2Oracle: path must be longer than 2"); + + address tokenIn_ = paths_[i][0]; + + require(_paths[tokenIn_].length == 0, "UniswapV2Oracle: path already registered"); + + for (uint256 j = 0; j < pathLength_ - 1; j++) { + (bool exists_, address pair_) = _pairExists(paths_[i][j], paths_[i][j + 1]); + require(exists_, "UniswapV2Oracle: uniswap pair doesn't exist"); + + _pairs.add(pair_); + _pairInfos[pair_].refs++; + } + + _paths[tokenIn_] = paths_[i]; + } + + updatePrices(); + } + + /** + * @notice The function to remove multiple token paths from the oracle. Unregisters the pairs as well + * @param tokenIns_ The array of token addresses to remove + */ + function _removePaths(address[] memory tokenIns_) internal { + uint256 numberOfPaths_ = tokenIns_.length; + + for (uint256 i = 0; i < numberOfPaths_; i++) { + address tokenIn_ = tokenIns_[i]; + uint256 pathLength_ = _paths[tokenIn_].length; + + if (pathLength_ == 0) { + continue; + } + + for (uint256 j = 0; j < pathLength_ - 1; j++) { + address pair_ = uniswapV2Factory.getPair( + _paths[tokenIn_][j], + _paths[tokenIn_][j + 1] + ); + + PairInfo storage _pairInfo = _pairInfos[pair_]; + + /// @dev can't underflow + _pairInfo.refs--; + + if (_pairInfo.refs == 0) { + _pairs.remove(pair_); + } + } + + delete _paths[tokenIn_]; + } + } + + /** + * @notice The private function to get the price of a token inside a pair + */ + function _getPrice(address pair_, address expectedToken_) private view returns (uint256) { + PairInfo storage pairInfo = _pairInfos[pair_]; + + unchecked { + /// @dev pairInfo.blockTimestamps can't be empty + uint256 index_ = pairInfo.blockTimestamps.lowerBound( + (uint32(block.timestamp) - timeWindow) % 2 ** 32 + ); + index_ = index_ == 0 ? index_ : index_ - 1; + + uint256 price0CumulativeOld_ = pairInfo.prices0Cumulative[index_]; + uint256 price1CumulativeOld_ = pairInfo.prices1Cumulative[index_]; + uint256 blockTimestampOld_ = pairInfo.blockTimestamps[index_]; + + uint256 price0_; + uint256 price1_; + + (uint256 price0Cumulative_, uint256 price1Cumulative_, uint256 blockTimestamp_) = pair_ + .currentCumulativePrices(); + + price0_ = + (price0Cumulative_ - price0CumulativeOld_) / + (blockTimestamp_ - blockTimestampOld_); + price1_ = + (price1Cumulative_ - price1CumulativeOld_) / + (blockTimestamp_ - blockTimestampOld_); + + return expectedToken_ == IUniswapV2Pair(pair_).token0() ? price0_ : price1_; + } + } + + /** + * @notice The private function to check the existence of a pair + */ + function _pairExists(address token1_, address token2_) private view returns (bool, address) { + address pair_ = uniswapV2Factory.getPair(token1_, token2_); + + return (pair_ != address(0), pair_); + } +} diff --git a/contracts/package.json b/contracts/package.json index 213273c3..b659f1e9 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@solarity/solidity-lib", - "version": "2.5.11", + "version": "2.5.12", "license": "MIT", "author": "Distributed Lab", "readme": "README.md", @@ -22,6 +22,8 @@ ], "dependencies": { "@openzeppelin/contracts": "4.9.2", - "@openzeppelin/contracts-upgradeable": "4.9.2" + "@openzeppelin/contracts-upgradeable": "4.9.2", + "@uniswap/v2-core": "1.0.1", + "@uniswap/v2-periphery": "1.1.0-beta.0" } } diff --git a/package-lock.json b/package-lock.json index c1b08d40..a37f8314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,19 @@ { "name": "@solarity/solidity-lib", - "version": "2.5.11", + "version": "2.5.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@solarity/solidity-lib", - "version": "2.5.11", + "version": "2.5.12", "hasInstallScript": true, "license": "MIT", "dependencies": { "@openzeppelin/contracts": "4.9.2", - "@openzeppelin/contracts-upgradeable": "4.9.2" + "@openzeppelin/contracts-upgradeable": "4.9.2", + "@uniswap/v2-core": "1.0.1", + "@uniswap/v2-periphery": "1.1.0-beta.0" }, "devDependencies": { "@metamask/eth-sig-util": "^7.0.0", @@ -3089,9 +3091,9 @@ } }, "node_modules/@types/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-/k+vesl92vMvMygmQrFe9Aimxi6oQXFUX9mA5HanTrKUSAMoLauSi6PNFOdRw0oeqilaW600GNx2vSaT2f8aIQ==", "dev": true }, "node_modules/@types/chai-as-promised": { @@ -3180,9 +3182,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.18.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.3.tgz", - "integrity": "sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA==", + "version": "18.18.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.4.tgz", + "integrity": "sha512-t3rNFBgJRugIhackit2mVcLfF6IRc0JE4oeizPQL8Zrm8n2WY/0wOdpOPhdtG0V9Q2TlW/axbF1MJ6z+Yj/kKQ==", "dev": true }, "node_modules/@types/pbkdf2": { @@ -3240,6 +3242,42 @@ "@types/node": "*" } }, + "node_modules/@uniswap/lib": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@uniswap/lib/-/lib-1.1.1.tgz", + "integrity": "sha512-2yK7sLpKIT91TiS5sewHtOa7YuM8IuBXVl4GZv2jZFys4D2sY7K5vZh6MqD25TPA95Od+0YzCVq6cTF2IKrOmg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v2-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.1.tgz", + "integrity": "sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v2-periphery": { + "version": "1.1.0-beta.0", + "resolved": "https://registry.npmjs.org/@uniswap/v2-periphery/-/v2-periphery-1.1.0-beta.0.tgz", + "integrity": "sha512-6dkwAMKza8nzqYiXEr2D86dgW3TTavUvCR0w2Tu33bAbM8Ah43LKAzH7oKKPRT5VJQaMi1jtkGs1E8JPor1n5g==", + "dependencies": { + "@uniswap/lib": "1.1.1", + "@uniswap/v2-core": "1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v2-periphery/node_modules/@uniswap/v2-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.0.tgz", + "integrity": "sha512-BJiXrBGnN8mti7saW49MXwxDBRFiWemGetE58q8zgfnPPzQKq55ADltEILqOt6VFZ22kVeVKbF8gVd8aY3l7pA==", + "engines": { + "node": ">=10" + } + }, "node_modules/abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -6489,9 +6527,9 @@ } }, "node_modules/hardhat": { - "version": "2.17.4", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.17.4.tgz", - "integrity": "sha512-YTyHjVc9s14CY/O7Dbtzcr/92fcz6AzhrMaj6lYsZpYPIPLzOrFCZHHPxfGQB6FiE6IPNE0uJaAbr7zGF79goA==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.18.0.tgz", + "integrity": "sha512-Com3SPFgk6v73LlE3rypuh32DYxOWvNbVBm5xfPUEzGVEW54Fcc4j3Uq7j6COj7S8Jc27uNihLFsveHYM0YJkQ==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.1.2", @@ -7689,9 +7727,9 @@ } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -10805,9 +10843,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz", - "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, "node_modules/sprintf-js": { @@ -15110,9 +15148,9 @@ } }, "@types/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-/k+vesl92vMvMygmQrFe9Aimxi6oQXFUX9mA5HanTrKUSAMoLauSi6PNFOdRw0oeqilaW600GNx2vSaT2f8aIQ==", "dev": true }, "@types/chai-as-promised": { @@ -15201,9 +15239,9 @@ "dev": true }, "@types/node": { - "version": "18.18.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.3.tgz", - "integrity": "sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA==", + "version": "18.18.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.4.tgz", + "integrity": "sha512-t3rNFBgJRugIhackit2mVcLfF6IRc0JE4oeizPQL8Zrm8n2WY/0wOdpOPhdtG0V9Q2TlW/axbF1MJ6z+Yj/kKQ==", "dev": true }, "@types/pbkdf2": { @@ -15263,6 +15301,32 @@ "@types/node": "*" } }, + "@uniswap/lib": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@uniswap/lib/-/lib-1.1.1.tgz", + "integrity": "sha512-2yK7sLpKIT91TiS5sewHtOa7YuM8IuBXVl4GZv2jZFys4D2sY7K5vZh6MqD25TPA95Od+0YzCVq6cTF2IKrOmg==" + }, + "@uniswap/v2-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.1.tgz", + "integrity": "sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==" + }, + "@uniswap/v2-periphery": { + "version": "1.1.0-beta.0", + "resolved": "https://registry.npmjs.org/@uniswap/v2-periphery/-/v2-periphery-1.1.0-beta.0.tgz", + "integrity": "sha512-6dkwAMKza8nzqYiXEr2D86dgW3TTavUvCR0w2Tu33bAbM8Ah43LKAzH7oKKPRT5VJQaMi1jtkGs1E8JPor1n5g==", + "requires": { + "@uniswap/lib": "1.1.1", + "@uniswap/v2-core": "1.0.0" + }, + "dependencies": { + "@uniswap/v2-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.0.tgz", + "integrity": "sha512-BJiXrBGnN8mti7saW49MXwxDBRFiWemGetE58q8zgfnPPzQKq55ADltEILqOt6VFZ22kVeVKbF8gVd8aY3l7pA==" + } + } + }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -17847,9 +17911,9 @@ } }, "hardhat": { - "version": "2.17.4", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.17.4.tgz", - "integrity": "sha512-YTyHjVc9s14CY/O7Dbtzcr/92fcz6AzhrMaj6lYsZpYPIPLzOrFCZHHPxfGQB6FiE6IPNE0uJaAbr7zGF79goA==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.18.0.tgz", + "integrity": "sha512-Com3SPFgk6v73LlE3rypuh32DYxOWvNbVBm5xfPUEzGVEW54Fcc4j3Uq7j6COj7S8Jc27uNihLFsveHYM0YJkQ==", "dev": true, "requires": { "@ethersproject/abi": "^5.1.2", @@ -18759,9 +18823,9 @@ } }, "keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "requires": { "json-buffer": "3.0.1" @@ -21184,9 +21248,9 @@ } }, "spdx-license-ids": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz", - "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, "sprintf-js": { diff --git a/package.json b/package.json index 9e55576a..33032905 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solarity/solidity-lib", - "version": "2.5.11", + "version": "2.5.12", "license": "MIT", "author": "Distributed Lab", "description": "Solidity Library by Distributed Lab", @@ -30,7 +30,9 @@ }, "dependencies": { "@openzeppelin/contracts": "4.9.2", - "@openzeppelin/contracts-upgradeable": "4.9.2" + "@openzeppelin/contracts-upgradeable": "4.9.2", + "@uniswap/v2-core": "1.0.1", + "@uniswap/v2-periphery": "1.1.0-beta.0" }, "devDependencies": { "@solarity/hardhat-markup": "^1.0.3", diff --git a/test/oracles/UniswapV2Oracle.test.ts b/test/oracles/UniswapV2Oracle.test.ts new file mode 100644 index 00000000..b0f1d663 --- /dev/null +++ b/test/oracles/UniswapV2Oracle.test.ts @@ -0,0 +1,214 @@ +import { ethers } from "hardhat"; +import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { wei } from "@/scripts/utils/utils"; +import { Reverter } from "@/test/helpers/reverter"; + +import { UniswapV2FactoryMock, UniswapV2OracleMock, UniswapV2PairMock } from "@ethers-v6"; + +describe("UniswapV2Oracle", () => { + const reverter = new Reverter(); + + const ORACLE_TIME_WINDOW = 1; + + const A_TOKEN = "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa"; + const B_TOKEN = "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"; + const C_TOKEN = "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"; + const A_C_PATH = [A_TOKEN, C_TOKEN]; + const C_A_C_PATH = [C_TOKEN, A_TOKEN, C_TOKEN]; + const B_A_C_PATH = [B_TOKEN, A_TOKEN, C_TOKEN]; + + let oracle: UniswapV2OracleMock; + let uniswapV2Factory: UniswapV2FactoryMock; + let A_C_PAIR: string; + let A_B_PAIR: string; + + before("setup", async () => { + const UniswapV2FactoryMock = await ethers.getContractFactory("UniswapV2FactoryMock"); + const Oracle = await ethers.getContractFactory("UniswapV2OracleMock"); + + uniswapV2Factory = await UniswapV2FactoryMock.deploy(); + oracle = await Oracle.deploy(); + + await oracle.__OracleV2Mock_init(await uniswapV2Factory.getAddress(), ORACLE_TIME_WINDOW); + + await reverter.snapshot(); + }); + + async function createPairs() { + await uniswapV2Factory.createPair(A_TOKEN, C_TOKEN); + await uniswapV2Factory.createPair(A_TOKEN, B_TOKEN); + + A_C_PAIR = await uniswapV2Factory.getPair(A_TOKEN, C_TOKEN); + A_B_PAIR = await uniswapV2Factory.getPair(A_TOKEN, B_TOKEN); + } + + afterEach(reverter.revert); + + describe("init", () => { + it("should set oracle correctly", async () => { + expect(await oracle.uniswapV2Factory()).to.equal(await uniswapV2Factory.getAddress()); + expect(await oracle.timeWindow()).to.equal(ORACLE_TIME_WINDOW); + }); + + it("should not initialize twice", async () => { + await expect(oracle.mockInit(await uniswapV2Factory.getAddress(), ORACLE_TIME_WINDOW)).to.be.revertedWith( + "Initializable: contract is not initializing" + ); + await expect( + oracle.__OracleV2Mock_init(await uniswapV2Factory.getAddress(), ORACLE_TIME_WINDOW) + ).to.be.revertedWith("Initializable: contract is already initialized"); + }); + }); + + describe("set", () => { + it("should set timewindow correctly", async () => { + await oracle.setTimeWindow(20); + + expect(await oracle.timeWindow()).to.equal(20); + }); + + it("shouldn't set 0 timewindow", async () => { + await expect(oracle.setTimeWindow(0)).to.be.revertedWith("UniswapV2Oracle: time window can't be 0"); + }); + + it("should add paths correctly", async () => { + await createPairs(); + + await oracle.addPaths([A_C_PATH, B_A_C_PATH]); + + expect(await oracle.getPath(A_TOKEN)).to.deep.equal(A_C_PATH); + expect(await oracle.getPath(B_TOKEN)).to.deep.equal(B_A_C_PATH); + + expect(await oracle.getPairs()).to.deep.equal([A_C_PAIR, A_B_PAIR]); + }); + + it("should not allow to set path with length < 2", async () => { + await expect(oracle.addPaths([[C_TOKEN]])).to.be.revertedWith("UniswapV2Oracle: path must be longer than 2"); + }); + + it("should not allow to set path with non-existent pairs", async () => { + await expect(oracle.addPaths([A_C_PATH])).to.be.revertedWith("UniswapV2Oracle: uniswap pair doesn't exist"); + }); + + it("should not add same path twice", async () => { + await createPairs(); + + await expect(oracle.addPaths([A_C_PATH, A_C_PATH])).to.be.revertedWith( + "UniswapV2Oracle: path already registered" + ); + }); + }); + + describe("remove", () => { + it("should remove paths correctly", async () => { + await createPairs(); + + await oracle.addPaths([A_C_PATH]); + await oracle.addPaths([B_A_C_PATH]); + await oracle.addPaths([C_A_C_PATH]); + + await oracle.removePaths([A_TOKEN]); + + expect(await oracle.getPath(A_TOKEN)).to.deep.equal([]); + expect(await oracle.getPairs()).to.deep.equal([A_C_PAIR, A_B_PAIR]); + + await oracle.removePaths([B_TOKEN, B_TOKEN]); + + expect(await oracle.getPath(B_TOKEN)).to.deep.equal([]); + expect(await oracle.getPairs()).to.deep.equal([A_C_PAIR]); + + await oracle.removePaths([C_TOKEN]); + + expect(await oracle.getPath(C_TOKEN)).to.deep.equal([]); + expect(await oracle.getPairs()).to.deep.equal([]); + }); + }); + + describe("update", () => { + it("should update price correctly", async () => { + await createPairs(); + + await oracle.addPaths([A_C_PATH]); + + let rounds = await oracle.getPairRounds(A_C_PAIR); + let pairInfo = await oracle.getPairInfo(A_C_PAIR, 0); + + expect(rounds).to.equal(1); + expect(pairInfo[2]).to.equal((await time.latest()) % 2 ** 32); + + await oracle.updatePrices(); + + rounds = await oracle.getPairRounds(A_C_PAIR); + + expect(rounds).to.equal(2); + }); + + it("should not update if block is the same or later", async () => { + await createPairs(); + + await oracle.addPaths([A_C_PATH]); + + await oracle.doubleUpdatePrice(); + + let rounds = await oracle.getPairRounds(A_C_PAIR); + + expect(rounds).to.equal(2); + }); + }); + + describe("getPrice", () => { + it("should correctly get price", async () => { + await createPairs(); + + await oracle.addPaths([A_C_PATH]); + + await oracle.updatePrices(); + + let response = await oracle.getPrice(A_TOKEN, 10); + + expect(response[0]).to.equal(10); + expect(response[1]).to.equal(C_TOKEN); + }); + + it("should correctly get complex price", async () => { + await createPairs(); + + await oracle.addPaths([A_C_PATH]); + + const firstTime = await time.latest(); + + await time.increaseTo(firstTime + 10); + + const pair = await ethers.getContractAt("UniswapV2PairMock", A_C_PAIR); + + await pair.swap(wei("0.85"), 0); + + await time.increaseTo(firstTime + 20); + + let response = await oracle.getPrice(A_TOKEN, 10); + + expect(response[0]).to.equal("35"); + expect(response[1]).to.equal(C_TOKEN); + }); + + it("should return 0 price", async () => { + await createPairs(); + + await oracle.addPaths([B_A_C_PATH]); + + const pair = await ethers.getContractAt("UniswapV2PairMock", A_C_PAIR); + + await pair.swap(wei("1"), 0); + + let response = await oracle.getPrice(B_TOKEN, 0); + + expect(response[0]).to.equal("0"); + expect(response[1]).to.equal(C_TOKEN); + }); + + it("should not get price if there is no path", async () => { + await expect(oracle.getPrice(A_TOKEN, 10)).to.be.revertedWith("UniswapV2Oracle: invalid path"); + }); + }); +});