├── .env.example ├── .gitattributes ├── .gitignore ├── tsconfig.json ├── README.md ├── scripts └── deploy.ts ├── contracts ├── libraries │ └── UQ112x112.sol ├── interfaces │ ├── ITarotPriceOracle.sol │ ├── IERC20.sol │ └── IUniswapV2Pair.sol └── TarotPriceOracle.sol ├── package.json ├── hardhat.config.ts └── LICENSE /.env.example: -------------------------------------------------------------------------------- 1 | MNEMONIC= 2 | FTMSCAN_API_KEY= 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | 4 | #Hardhat files 5 | cache 6 | artifacts 7 | 8 | typechain 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "resolveJsonModule": true 9 | }, 10 | "include": ["./scripts", "./test"], 11 | "files": ["./hardhat.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tarot Price Oracle 2 | 3 | A maintenance-free, decentralized, manipulation-resistant price oracle for use with implementations of [IUniswapV2Pair](https://uniswap.org/docs/v2/smart-contracts/pair/). 4 | 5 | Contract on Fantom Opera: [0x36Df0A76a124d8b2205fA11766eC2eFF8Ce38A35](https://ftmscan.com/address/0x36Df0A76a124d8b2205fA11766eC2eFF8Ce38A35#code) 6 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | async function main() { 4 | const factory = await ethers.getContractFactory("TarotPriceOracle"); 5 | const contract = await factory.deploy(); 6 | 7 | console.log(`Contract address: ${contract.address}`); 8 | console.log(`Contract deploy tx hash: ${contract.deployTransaction.hash}`); 9 | 10 | await contract.deployed(); 11 | 12 | console.log('Finished'); 13 | } 14 | 15 | main() 16 | .then(() => process.exit(0)) 17 | .catch((error) => { 18 | console.error(error); 19 | process.exit(1); 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /contracts/libraries/UQ112x112.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | // a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) 4 | 5 | // range: [0, 2**112 - 1] 6 | // resolution: 1 / 2**112 7 | 8 | library UQ112x112 { 9 | uint224 constant Q112 = 2**112; 10 | 11 | // encode a uint112 as a UQ112x112 12 | function encode(uint112 y) internal pure returns (uint224 z) { 13 | z = uint224(y) * Q112; // never overflows 14 | } 15 | 16 | // divide a UQ112x112 by a uint112, returning a UQ112x112 17 | function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) { 18 | z = x / uint224(y); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tarot-price-oracle", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "npm run compile", 7 | "compile": "npx hardhat compile", 8 | "test": "npx hardhat test" 9 | }, 10 | "devDependencies": { 11 | "@nomiclabs/hardhat-ethers": "^2.0.2", 12 | "@nomiclabs/hardhat-etherscan": "^2.1.2", 13 | "@nomiclabs/hardhat-waffle": "^2.0.1", 14 | "@typechain/ethers-v5": "^7.0.0", 15 | "@typechain/hardhat": "^2.0.1", 16 | "@types/chai": "^4.2.18", 17 | "@types/mocha": "^8.2.2", 18 | "@types/node": "^15.12.1", 19 | "chai": "^4.3.4", 20 | "dotenv": "^10.0.0", 21 | "ethereum-waffle": "^3.3.0", 22 | "ethers": "^5.3.0", 23 | "hardhat": "^2.3.0", 24 | "ts-node": "^10.0.0", 25 | "typechain": "^5.0.0", 26 | "typescript": "^4.3.2" 27 | }, 28 | "dependencies": {} 29 | } 30 | -------------------------------------------------------------------------------- /contracts/interfaces/ITarotPriceOracle.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | interface ITarotPriceOracle { 4 | event PriceUpdate( 5 | address indexed pair, 6 | uint256 priceCumulative, 7 | uint32 blockTimestamp, 8 | bool latestIsSlotA 9 | ); 10 | 11 | function MIN_T() external pure returns (uint32); 12 | 13 | function getPair(address uniswapV2Pair) 14 | external 15 | view 16 | returns ( 17 | uint256 priceCumulativeSlotA, 18 | uint256 priceCumulativeSlotB, 19 | uint32 lastUpdateSlotA, 20 | uint32 lastUpdateSlotB, 21 | bool latestIsSlotA, 22 | bool initialized 23 | ); 24 | 25 | function initialize(address uniswapV2Pair) external; 26 | 27 | function getResult(address uniswapV2Pair) 28 | external 29 | returns (uint224 price, uint32 T); 30 | 31 | function getBlockTimestamp() external view returns (uint32); 32 | } 33 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | interface IERC20 { 4 | event Approval( 5 | address indexed owner, 6 | address indexed spender, 7 | uint256 value 8 | ); 9 | event Transfer(address indexed from, address indexed to, uint256 value); 10 | 11 | function name() external view returns (string memory); 12 | 13 | function symbol() external view returns (string memory); 14 | 15 | function decimals() external view returns (uint8); 16 | 17 | function totalSupply() external view returns (uint256); 18 | 19 | function balanceOf(address owner) external view returns (uint256); 20 | 21 | function allowance(address owner, address spender) 22 | external 23 | view 24 | returns (uint256); 25 | 26 | function approve(address spender, uint256 value) external returns (bool); 27 | 28 | function transfer(address to, uint256 value) external returns (bool); 29 | 30 | function transferFrom( 31 | address from, 32 | address to, 33 | uint256 value 34 | ) external returns (bool); 35 | } 36 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config(); 3 | import {HardhatUserConfig, HttpNetworkConfig, HttpNetworkHDAccountsConfig} from 'hardhat/types'; 4 | import '@typechain/hardhat' 5 | import '@nomiclabs/hardhat-ethers' 6 | import '@nomiclabs/hardhat-waffle' 7 | import "@nomiclabs/hardhat-etherscan"; 8 | 9 | const config: HardhatUserConfig = { 10 | solidity: { 11 | version: '0.5.16', 12 | settings: { 13 | optimizer: { 14 | enabled: true, 15 | runs: 200, 16 | }, 17 | }, 18 | }, 19 | defaultNetwork: 'fantomtestnet', 20 | networks: { 21 | hardhat: {}, 22 | fantom: { 23 | chainId: 250, 24 | url: 'https://rpcapi.fantom.network', 25 | accounts: { 26 | mnemonic: process.env.MNEMONIC 27 | } 28 | }, 29 | fantomtestnet: { 30 | chainId: 4002, 31 | url: 'https://rpc.testnet.fantom.network', 32 | accounts: { 33 | mnemonic: process.env.MNEMONIC 34 | } 35 | } 36 | }, 37 | etherscan: { 38 | apiKey: process.env.FTMSCAN_API_KEY 39 | } 40 | }; 41 | 42 | export default config; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tarot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Pair.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | interface IUniswapV2Pair { 4 | event Approval( 5 | address indexed owner, 6 | address indexed spender, 7 | uint256 value 8 | ); 9 | event Transfer(address indexed from, address indexed to, uint256 value); 10 | 11 | function name() external view returns (string memory); 12 | 13 | function symbol() external view returns (string memory); 14 | 15 | function decimals() external view returns (uint8); 16 | 17 | function totalSupply() external view returns (uint256); 18 | 19 | function balanceOf(address owner) external view returns (uint256); 20 | 21 | function allowance(address owner, address spender) 22 | external 23 | view 24 | returns (uint256); 25 | 26 | function approve(address spender, uint256 value) external returns (bool); 27 | 28 | function transfer(address to, uint256 value) external returns (bool); 29 | 30 | function transferFrom( 31 | address from, 32 | address to, 33 | uint256 value 34 | ) external returns (bool); 35 | 36 | function token0() external view returns (address); 37 | 38 | function token1() external view returns (address); 39 | 40 | function getReserves() 41 | external 42 | view 43 | returns ( 44 | uint112 reserve0, 45 | uint112 reserve1, 46 | uint32 blockTimestampLast 47 | ); 48 | 49 | function price0CumulativeLast() external view returns (uint256); 50 | 51 | function price1CumulativeLast() external view returns (uint256); 52 | } 53 | -------------------------------------------------------------------------------- /contracts/TarotPriceOracle.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | import "./libraries/UQ112x112.sol"; 4 | import "./interfaces/IUniswapV2Pair.sol"; 5 | import "./interfaces/ITarotPriceOracle.sol"; 6 | 7 | contract TarotPriceOracle is ITarotPriceOracle { 8 | using UQ112x112 for uint224; 9 | 10 | uint32 public constant MIN_T = 1200; 11 | 12 | struct Pair { 13 | uint256 priceCumulativeSlotA; 14 | uint256 priceCumulativeSlotB; 15 | uint32 lastUpdateSlotA; 16 | uint32 lastUpdateSlotB; 17 | bool latestIsSlotA; 18 | bool initialized; 19 | } 20 | mapping(address => Pair) public getPair; 21 | 22 | event PriceUpdate( 23 | address indexed pair, 24 | uint256 priceCumulative, 25 | uint32 blockTimestamp, 26 | bool latestIsSlotA 27 | ); 28 | 29 | function toUint224(uint256 input) internal pure returns (uint224) { 30 | require(input <= uint224(-1), "TarotPriceOracle: UINT224_OVERFLOW"); 31 | return uint224(input); 32 | } 33 | 34 | function getPriceCumulativeCurrent(address uniswapV2Pair) 35 | internal 36 | view 37 | returns (uint256 priceCumulative) 38 | { 39 | priceCumulative = IUniswapV2Pair(uniswapV2Pair).price0CumulativeLast(); 40 | (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = 41 | IUniswapV2Pair(uniswapV2Pair).getReserves(); 42 | uint224 priceLatest = UQ112x112.encode(reserve1).uqdiv(reserve0); 43 | uint32 timeElapsed = getBlockTimestamp() - blockTimestampLast; // overflow is desired 44 | // * never overflows, and + overflow is desired 45 | priceCumulative += uint256(priceLatest) * timeElapsed; 46 | } 47 | 48 | function initialize(address uniswapV2Pair) external { 49 | Pair storage pairStorage = getPair[uniswapV2Pair]; 50 | require( 51 | !pairStorage.initialized, 52 | "TarotPriceOracle: ALREADY_INITIALIZED" 53 | ); 54 | 55 | uint256 priceCumulativeCurrent = 56 | getPriceCumulativeCurrent(uniswapV2Pair); 57 | uint32 blockTimestamp = getBlockTimestamp(); 58 | pairStorage.priceCumulativeSlotA = priceCumulativeCurrent; 59 | pairStorage.priceCumulativeSlotB = priceCumulativeCurrent; 60 | pairStorage.lastUpdateSlotA = blockTimestamp; 61 | pairStorage.lastUpdateSlotB = blockTimestamp; 62 | pairStorage.latestIsSlotA = true; 63 | pairStorage.initialized = true; 64 | emit PriceUpdate( 65 | uniswapV2Pair, 66 | priceCumulativeCurrent, 67 | blockTimestamp, 68 | true 69 | ); 70 | } 71 | 72 | function getResult(address uniswapV2Pair) 73 | external 74 | returns (uint224 price, uint32 T) 75 | { 76 | Pair memory pair = getPair[uniswapV2Pair]; 77 | require(pair.initialized, "TarotPriceOracle: NOT_INITIALIZED"); 78 | Pair storage pairStorage = getPair[uniswapV2Pair]; 79 | 80 | uint32 blockTimestamp = getBlockTimestamp(); 81 | uint32 lastUpdateTimestamp = 82 | pair.latestIsSlotA ? pair.lastUpdateSlotA : pair.lastUpdateSlotB; 83 | uint256 priceCumulativeCurrent = 84 | getPriceCumulativeCurrent(uniswapV2Pair); 85 | uint256 priceCumulativeLast; 86 | 87 | if (blockTimestamp - lastUpdateTimestamp >= MIN_T) { 88 | // update price 89 | priceCumulativeLast = pair.latestIsSlotA 90 | ? pair.priceCumulativeSlotA 91 | : pair.priceCumulativeSlotB; 92 | if (pair.latestIsSlotA) { 93 | pairStorage.priceCumulativeSlotB = priceCumulativeCurrent; 94 | pairStorage.lastUpdateSlotB = blockTimestamp; 95 | } else { 96 | pairStorage.priceCumulativeSlotA = priceCumulativeCurrent; 97 | pairStorage.lastUpdateSlotA = blockTimestamp; 98 | } 99 | pairStorage.latestIsSlotA = !pair.latestIsSlotA; 100 | emit PriceUpdate( 101 | uniswapV2Pair, 102 | priceCumulativeCurrent, 103 | blockTimestamp, 104 | !pair.latestIsSlotA 105 | ); 106 | } else { 107 | // don't update; return price using previous priceCumulative 108 | lastUpdateTimestamp = pair.latestIsSlotA 109 | ? pair.lastUpdateSlotB 110 | : pair.lastUpdateSlotA; 111 | priceCumulativeLast = pair.latestIsSlotA 112 | ? pair.priceCumulativeSlotB 113 | : pair.priceCumulativeSlotA; 114 | } 115 | 116 | T = blockTimestamp - lastUpdateTimestamp; // overflow is desired 117 | require(T >= MIN_T, "TarotPriceOracle: NOT_READY"); //reverts only if the pair has just been initialized 118 | // / is safe, and - overflow is desired 119 | price = toUint224((priceCumulativeCurrent - priceCumulativeLast) / T); 120 | } 121 | 122 | /*** Utilities ***/ 123 | 124 | function getBlockTimestamp() public view returns (uint32) { 125 | return uint32(block.timestamp % 2**32); 126 | } 127 | } 128 | --------------------------------------------------------------------------------