Skip to content

Commit

Permalink
Feat/uniswap v3 oracle (#73)
Browse files Browse the repository at this point in the history
* UniswapV3Oracle added

* fix tests

* Fixed tests

* Update UniswapV3Oracle.test.ts

* coverage 100% for oracle v3

* removed UniswapV3PoolDeployer mock

* renaming

* codestyle

mock optimization

* minor fixes

oracle lib moved to mocks

* Update UniswapV3Oracle.test.ts

* period check moved

* review fixes

* Update UniswapV3Oracle.sol

* small logic fixes

* fixed decimals logic

no need to deploy tokens in tests
more precise tests

* changed ito uint128 price

fixes in findOldestObservation
added unchecked to tickHelper

* review fixes

_findOldestObservation -> getOldestObservationSecondsAgo in TickHelper
private getPrice returns uint128 now

* moved uni libraries to vendor folder

* fixes

test became more readable

* imoroved tests

vendor to original codestyle

* plus test case for tick logic

* test optimization

reason strings in vendor returned to original

* versions

test optimization

* test update

* Update UniswapV3Oracle.test.ts

* Update UniswapV3Oracle.test.ts

* Update UniswapV3Oracle.test.ts

* added bignumber.js to dependencies

* dependencies update

* oracleV3 with compiler version <0.8.0

uniswap libs are used through imports now
removed vendor folder
updated decription

* Update UniswapV3Oracle.sol

* package fixes

---------

Co-authored-by: Artem Chystiakov <[email protected]>
  • Loading branch information
aritkulova and Arvolear authored Dec 6, 2023
1 parent ddaf3b0 commit 07cb309
Show file tree
Hide file tree
Showing 34 changed files with 5,505 additions and 12,307 deletions.
6 changes: 0 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,2 @@
# keys
COINMARKETCAP_KEY=COINMARKETCAP API KEY

# By default 'ethers-v6'
TYPECHAIN_TARGET=TYPECHAIN TARGET

# Set 'false' to not automatically generate types
TYPECHAIN_FORCE=TYPECHAIN FORCE
1 change: 1 addition & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"plugins": ["prettier-plugin-solidity"],
"overrides": [
{
"files": "*.sol",
Expand Down
2 changes: 1 addition & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"extends": "solhint:recommended",
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "warn",
"reentrancy": "off",
"prettier/prettier": "off",
"modifier-name-mixedcase": "off",
"no-empty-blocks": "off",
"func-visibility": "off",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The library consists of modules and utilities that are built with a help of [Ope
- Optimized [**Incremental Merkle Tree**](https://github.com/runtimeverification/deposit-contract-verification/blob/master/deposit-contract-verification.pdf) data structure
- Novel **ReturnDataProxy** contract
- Lightweight **SBT** implementation
- Flexible UniswapV2 oracle
- Flexible UniswapV2 and UniswapV3 oracles
- Utilities to ease work with ERC20 decimals, arrays, sets and ZK proofs

## Overview
Expand Down
33 changes: 33 additions & 0 deletions contracts/mock/oracles/uniswap-v3/UniswapV3FactoryMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.8.0;

import {UniswapV3PoolMock} from "./UniswapV3PoolMock.sol";

contract UniswapV3FactoryMock {
mapping(address => mapping(address => mapping(uint24 => address))) public getPool;

function createPool(
address tokenA_,
address tokenB_,
uint24 fee_
) external returns (address pool_) {
(address token0_, address token1_) = tokenA_ < tokenB_
? (tokenA_, tokenB_)
: (tokenB_, tokenA_);

pool_ = _deploy(token0_, token1_, fee_);

getPool[token0_][token1_][fee_] = pool_;
getPool[token1_][token0_][fee_] = pool_;
}

function _deploy(
address token0_,
address token1_,
uint24 fee_
) internal returns (address pool_) {
pool_ = address(
new UniswapV3PoolMock{salt: keccak256(abi.encode(token0_, token1_, fee_))}()
);
}
}
85 changes: 85 additions & 0 deletions contracts/mock/oracles/uniswap-v3/UniswapV3PoolMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.8.0;

import {Oracle} from "@uniswap/v3-core/contracts/libraries/Oracle.sol";
import {TickMath} from "@uniswap/v3-core/contracts/libraries/TickMath.sol";

contract UniswapV3PoolMock {
using Oracle for Oracle.Observation[65535];

struct Slot0 {
uint160 sqrtPriceX96;
int24 tick;
uint16 observationIndex;
uint16 observationCardinality;
uint16 observationCardinalityNext;
uint8 feeProtocol;
bool unlocked;
}

Slot0 public slot0;
Oracle.Observation[65535] public observations;

function initialize(uint160 sqrtPriceX96_) external {
int24 tick_ = TickMath.getTickAtSqrtRatio(sqrtPriceX96_);

(uint16 cardinality_, uint16 cardinalityNext_) = observations.initialize(
_blockTimestamp()
);

slot0 = Slot0({
sqrtPriceX96: sqrtPriceX96_,
tick: tick_,
observationIndex: 0,
observationCardinality: cardinality_,
observationCardinalityNext: cardinalityNext_,
feeProtocol: 0,
unlocked: true
});
}

function addObservation(int24 tick_) external {
(slot0.observationIndex, slot0.observationCardinality) = observations.write(
slot0.observationIndex,
_blockTimestamp(),
slot0.tick,
0,
slot0.observationCardinality,
slot0.observationCardinalityNext
);

slot0.tick = tick_;
}

function increaseObservationCardinalityNext(uint16 observationCardinalityNext_) external {
slot0.observationCardinalityNext = observations.grow(
slot0.observationCardinalityNext,
observationCardinalityNext_
);
}

function observe(
uint32[] calldata secondAgos_
)
external
view
returns (
int56[] memory tickCumulatives_,
uint160[] memory secondsPerLiquidityCumulativeX128s_
)
{
return
observations.observe(
_blockTimestamp(),
secondAgos_,
slot0.tick,
slot0.observationIndex,
0,
slot0.observationCardinality
);
}

function _blockTimestamp() internal view virtual returns (uint32) {
return uint32(block.timestamp); // truncation is desired in original contract
}
}
119 changes: 119 additions & 0 deletions contracts/oracles/UniswapV3Oracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.8.0;

import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {FullMath} from "@uniswap/v3-core/contracts/libraries/FullMath.sol";
import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol";

/**
* @notice UniswapV3Oracle module
*
* A contract for retrieving prices from Uniswap V3 pools.
*
* Works by calculating the time-weighted average tick as difference between two tickCumulatives
* divided by number of second between them, tickCumulatives are taken from the newest observation
* and from the one nearest to required time.
*
* Price is obtained as 1.0001 in power of this tick.
*
* In case required period of time is unreachable, tick is taken from oldest available observation.
*/
contract UniswapV3Oracle {
using FullMath for *;

IUniswapV3Factory public immutable uniswapV3Factory;

/**
* @dev contract is not an Initializable because the compiler version is <0.8.0
*/
constructor(address uniswapV3Factory_) {
uniswapV3Factory = IUniswapV3Factory(uniswapV3Factory_);
}

/**
* @notice The function to retrieve the price of a token following the configured route
* @dev The function returns price in quote token decimals. If amount is zero, returns (0, 0)
* @param path_ the path of token address, the last one is token in which price will be returned
* @param fees_ the array of fees for particular pools
* @param amount_ the amount of baseToken_
* @param period_ the time period
* @return amount_ the price of start token in quote token
* @return minPeriod_ the oldest period for which there is an observation in case period_ time ago there was no observation
*/
function getPriceOfTokenInToken(
address[] memory path_,
uint24[] memory fees_,
uint128 amount_,
uint32 period_
) public view returns (uint128, uint32) {
uint256 pathLength_ = path_.length;

require(pathLength_ > 1, "UniswapV3Oracle: invalid path");
require(pathLength_ == fees_.length + 1, "UniswapV3Oracle: path/fee lengths do not match");
require(
block.timestamp > period_,
"UniswapV3Oracle: period larger than current timestamp"
);
require(period_ > 0, "UniswapV3Oracle: period can't be 0");

if (amount_ == 0) {
return (0, 0);
}

uint32 minPeriod_ = period_;

for (uint256 i = 0; i < pathLength_ - 1; i++) {
uint32 currentPeriod_;
(amount_, currentPeriod_) = _getPriceOfTokenInToken(
path_[i],
path_[i + 1],
amount_,
fees_[i],
period_
);

minPeriod_ = uint32(minPeriod_ < currentPeriod_ ? minPeriod_ : currentPeriod_);
}

return (amount_, minPeriod_);
}

/**
* @notice The private function to get the price of a token inside a pool
* @dev Price is expected to fit into uint128
*/
function _getPriceOfTokenInToken(
address baseToken_,
address quoteToken_,
uint128 amount_,
uint24 fee_,
uint32 period_
) private view returns (uint128, uint32) {
if (baseToken_ == quoteToken_) {
return (amount_, period_);
}

address pool_ = uniswapV3Factory.getPool(baseToken_, quoteToken_, fee_);

require(pool_ != address(0), "UniswapV3Oracle: such pool doesn't exist");

uint32 longestPeriod_ = OracleLibrary.getOldestObservationSecondsAgo(pool_);

require(
longestPeriod_ != 0,
"UniswapV3Oracle: the oldest observation is on the current block"
);

period_ = uint32(period_ < longestPeriod_ ? period_ : longestPeriod_);

(int24 arithmeticMeanTick, ) = OracleLibrary.consult(pool_, period_);

return (
uint128(
OracleLibrary.getQuoteAtTick(arithmeticMeanTick, amount_, baseToken_, quoteToken_)
),
period_
);
}
}
6 changes: 4 additions & 2 deletions contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solarity/solidity-lib",
"version": "2.6.9",
"version": "2.6.10",
"license": "MIT",
"author": "Distributed Lab",
"readme": "README.md",
Expand All @@ -24,6 +24,8 @@
"@openzeppelin/contracts": "4.9.2",
"@openzeppelin/contracts-upgradeable": "4.9.2",
"@uniswap/v2-core": "1.0.1",
"@uniswap/v2-periphery": "1.1.0-beta.0"
"@uniswap/v2-periphery": "1.1.0-beta.0",
"@uniswap/v3-core": "1.0.1",
"@uniswap/v3-periphery": "1.4.4"
}
}
45 changes: 22 additions & 23 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import "@nomiclabs/hardhat-web3";
import "@nomiclabs/hardhat-truffle5";
import "@nomicfoundation/hardhat-ethers";
import "@nomicfoundation/hardhat-chai-matchers";
import "@nomicfoundation/hardhat-network-helpers";
Expand All @@ -15,36 +13,38 @@ import { HardhatUserConfig } from "hardhat/config";
import * as dotenv from "dotenv";
dotenv.config();

function typechainTarget() {
const target = process.env.TYPECHAIN_TARGET;

return target === "" || target === undefined ? "ethers-v6" : target;
}

function forceTypechain() {
return process.env.TYPECHAIN_FORCE === "false";
}

const config: HardhatUserConfig = {
networks: {
hardhat: {
initialDate: "1970-01-01T00:00:00Z",
},
localhost: {
url: "http://127.0.0.1:8545",
initialDate: "1970-01-01T00:00:00Z",
gasMultiplier: 1.2,
},
},
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200,
compilers: [
{
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
evmVersion: "paris",
},
},
evmVersion: "paris",
},
{
version: "0.7.6",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
],
},
markup: {
onlyFiles: ["./contracts/"],
Expand All @@ -65,11 +65,10 @@ const config: HardhatUserConfig = {
coinmarketcap: `${process.env.COINMARKETCAP_KEY}`,
},
typechain: {
outDir: `generated-types/${typechainTarget().split("-")[0]}`,
target: typechainTarget(),
outDir: "generated-types/ethers",
target: "ethers-v6",
alwaysGenerateOverloads: true,
discriminateTypes: true,
dontOverrideCompile: forceTypechain(),
},
};

Expand Down
Loading

0 comments on commit 07cb309

Please sign in to comment.