├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .mocharc.json ├── .prettierignore ├── .prettierrc ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── Makefile ├── README.md ├── contracts ├── InstantUniswapPrice.sol ├── PowerOracle.sol ├── PowerOracleReader.sol ├── PowerOracleStorageV1.sol ├── PowerOracleTokenManagement.sol ├── PowerPoke.sol ├── PowerPokeStaking.sol ├── PowerPokeStakingStorageV1.sol ├── PowerPokeStorageV1.sol ├── Uniswap │ ├── UniswapV2Library.sol │ └── UniswapV2OracleLibrary.sol ├── UniswapTWAPProvider.sol ├── interfaces │ ├── BPoolInterface.sol │ ├── IEACAggregatorProxy.sol │ ├── IERC20Detailed.sol │ ├── IPowerOracleV2.sol │ ├── IPowerOracleV2Reader.sol │ ├── IPowerOracleV3.sol │ ├── IPowerOracleV3Reader.sol │ ├── IPowerOracleV3TokenManagement.sol │ ├── IPowerPoke.sol │ ├── IPowerPokeStaking.sol │ ├── IUniswapV2Factory.sol │ ├── IUniswapV2Pair.sol │ └── IUniswapV2Router02.sol ├── mocks │ ├── MockCToken.sol │ ├── MockCVP.sol │ ├── MockFastGasOracle.sol │ ├── MockOracle.sol │ ├── MockPoke.sol │ ├── MockProxyCall.sol │ ├── MockStaking.sol │ ├── MockUniswapRouter.sol │ ├── MockUniswapTokenPair.sol │ ├── MockWETH.sol │ ├── StubOracle.sol │ └── StubStaking.sol └── utils │ ├── PowerOwnable.sol │ └── PowerPausable.sol ├── hardhat.config.js ├── package.json ├── scripts └── gasUsedReport.sh ├── tasks ├── deployInstantUniswapPrice.js ├── deployMainnet.js ├── deployTestnet.js ├── fetchPairValues.js └── redeployOracleImplementation.js ├── test ├── Integration.test.js ├── PowerOracle.test.js ├── PowerOracleStaking.test.js ├── PowerPoke.js ├── builders.js ├── helpers.js └── localHelpers.js ├── truffle.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All files 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.sol] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts/ 3 | build/ 4 | cache/ 5 | coverage/ 6 | dist/ 7 | lib/ 8 | node_modules/ 9 | typechain/ 10 | tmp/ 11 | 12 | # files 13 | .solcover.js 14 | coverage.json 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended"], 3 | "root": true, 4 | "env": { 5 | "node": true, 6 | "mocha": true, 7 | "es2020": true 8 | }, 9 | "globals": { 10 | "web3": true, 11 | "artifacts": true, 12 | "contract": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020 16 | }, 17 | "rules": { 18 | "quotes": ["error", "single", "avoid-escape"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | node: ['12.x'] 14 | os: [ubuntu-latest] 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | - uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node }} 23 | 24 | - run: npm install -g yarn 25 | 26 | - id: yarn-cache 27 | run: echo "::set-output name=dir::$(yarn cache dir)" 28 | - uses: actions/cache@v1 29 | with: 30 | path: ${{ steps.yarn-cache.outputs.dir }} 31 | key: ${{ matrix.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 32 | restore-keys: | 33 | ${{ matrix.os }}-yarn- 34 | - run: yarn 35 | - run: yarn lint:sol 36 | - run: yarn lint:js 37 | - run: yarn compile && yarn test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .coverage_artifacts/ 3 | .coverage_cache/ 4 | .coverage_contracts/ 5 | .idea 6 | artifacts/ 7 | build/ 8 | cache/ 9 | coverage/ 10 | dist/ 11 | lib/ 12 | node_modules/ 13 | typechain/ 14 | 15 | # files 16 | *.env 17 | *.log 18 | *.tsbuildinfo 19 | coverage.json 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "delay": false, 3 | "exit": true, 4 | "extension": ["js"], 5 | "recursive": true, 6 | "require": ["hardhat/register"], 7 | "timeout": 50000 8 | } 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts/ 3 | build/ 4 | cache/ 5 | coverage/ 6 | dist/ 7 | lib/ 8 | node_modules/ 9 | typechain/ 10 | contracts/lib/FixedPoint.sol 11 | contracts/UniswapTWAPProvider.sol 12 | contracts/Uniswap/UniswapV2Library.sol 13 | contracts/Uniswap/UniswapV2OracleLibrary.sol 14 | contracts/utils/Ownable.sol 15 | contracts/utils/Pausable.sol 16 | 17 | # files 18 | coverage.json 19 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "printWidth": 120, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "overrides": [ 9 | { 10 | "files": "*.sol", 11 | "options": { 12 | "tabWidth": 2, 13 | "singleQuote": false 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | 3 | module.exports = { 4 | istanbulReporter: ['html'], 5 | providerOptions: { 6 | total_accounts: 30, 7 | default_balance_ether: BigInt(1e30).toString(), 8 | }, 9 | mocha: { 10 | delay: false, 11 | timeout: 70000 12 | }, 13 | onCompileComplete: async function (_config) { 14 | // await run("typechain"); 15 | }, 16 | onIstanbulComplete: async function (_config) { 17 | /* We need to do this because solcover generates bespoke artifacts. */ 18 | shell.rm('-rf', './artifacts'); 19 | shell.rm('-rf', './typechain'); 20 | }, 21 | skipFiles: ['mocks', 'test', 'InstantUniswapPrice.sol'], 22 | }; 23 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 7], 6 | "compiler-version": ["error", "^0.6.0"], 7 | "constructor-syntax": "error", 8 | "max-line-length": ["error", 120], 9 | "max-states-count": ["error", 20], 10 | "not-rely-on-time": "off", 11 | "prettier/prettier": "error", 12 | "avoid-low-level-calls": "off", 13 | "no-empty-blocks": "off", 14 | "reason-string": ["warn", { "maxLength": 64 }] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .yarn/ 3 | build/ 4 | dist/ 5 | node_modules/ 6 | contracts/lib/FixedPoint.sol 7 | contracts/UniswapTWAPProvider.sol 8 | contracts/Uniswap/UniswapLib.sol 9 | contracts/Uniswap/UniswapV2Library.sol 10 | contracts/Uniswap/UniswapV2OracleLibrary.sol 11 | contracts/interfaces/IUniswapV2Pair.sol 12 | contracts/utils/Ownable.sol 13 | contracts/utils/Pausable.sol 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | cleanup: 3 | rm -rf artifacts 4 | compile: cleanup 5 | yarn compile 6 | test: 7 | yarn test 8 | ctest: compile test 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Power Oracle Contracts 2 | 3 | [![Actions Status](https://github.com/powerpool-finance/power-oracle-contracts/workflows/CI/badge.svg)](https://github.com/powerpool-finance/power-oracle-contracts/actions) 4 | 5 | Power Oracle is a decentralized cross-chain price oracle working on Ethereum Main Network and sidechains. Power Oracle uses Uniswap V2 as its primary source of time-weighted average prices (TWAPs) and introduces economic incentives for independent price reporters. 6 | 7 | 🚨 **Security review status: unaudited** 8 | 9 | ## Contracts on Ethereum Main Network 10 | ### Active 11 | - `PowerOracle`(ProxyAdmin - [0x7696f9208f9e195ba31e6f4B2D07B6462C8C42bb](https://etherscan.io/address/0x7696f9208f9e195ba31e6f4B2D07B6462C8C42bb#code), Proxy - [0x50f8D7f4db16AA926497993F020364f739EDb988](https://etherscan.io/address/0x019e14DA4538ae1BF0BCd8608ab8595c6c6181FB#code), Implementation - [0xf0d67691dA5aD3813Aaf412756d61f0f4390c6d2](https://etherscan.io/address/0xf0d67691dA5aD3813Aaf412756d61f0f4390c6d2)). 12 | - `PowerPoke`(ProxyAdmin - [0x7696f9208f9e195ba31e6f4B2D07B6462C8C42bb](https://etherscan.io/address/0x7696f9208f9e195ba31e6f4B2D07B6462C8C42bb#code), Proxy - [0x04D7aA22ef7181eE3142F5063e026Af1BbBE5B96](https://etherscan.io/address/0x04D7aA22ef7181eE3142F5063e026Af1BbBE5B960x04D7aA22ef7181eE3142F5063e026Af1BbBE5B96#code), Implementation - [0xfE53Ad2c2085636FEBC20a9F06a0826659a5b059](https://etherscan.io/address/0xfE53Ad2c2085636FEBC20a9F06a0826659a5b059)). 13 | - `PowerPokeStaking`(ProxyAdmin - [0x7696f9208f9e195ba31e6f4B2D07B6462C8C42bb](https://etherscan.io/address/0x7696f9208f9e195ba31e6f4B2D07B6462C8C42bb#code), Proxy - [0x646E846b6eE143bDe4F329d4165929bbdcf425f5](https://etherscan.io/address/0x646E846b6eE143bDe4F329d4165929bbdcf425f5#code), Implementation - [0xc0Cd319c0066733C611fb9a8BD5f2A1c38EB74B2](https://etherscan.io/address/0xc0Cd319c0066733C611fb9a8BD5f2A1c38EB74B2)). 14 | 15 | ### Deprecated 16 | - `PowerOracle`(Implementation - [0xA394922A1A45786583e5383cf4485a6F325d8807](https://etherscan.io/address/0xA394922A1A45786583e5383cf4485a6F325d8807)). Previous implementation; 17 | - `PowerOracle`(Implementation - [0x4b6E556841a88B0682c0bc9AbB6bdAF4572184b4](https://etherscan.io/address/0x4b6E556841a88B0682c0bc9AbB6bdAF4572184b4)). Previous implementation; 18 | - `PowerOracle`(Implementation - [0x3359Bb31CD8F80a98a13856d3C89b71e7b51a0F0](https://etherscan.io/address/0x3359Bb31CD8F80a98a13856d3C89b71e7b51a0F0)). Previous implementation; 19 | - `PowerOracle`(Proxy - [0x019e14DA4538ae1BF0BCd8608ab8595c6c6181FB](https://etherscan.io/address/0x019e14DA4538ae1BF0BCd8608ab8595c6c6181FB)). Previous Proxy; 20 | 21 | ## Contracts on Kovan Test Network 22 | 23 | ## Testing and Development 24 | 25 | Use `yarn` or `npm` to run the following npm tasks: 26 | 27 | - `yarn compile` - compile contracts 28 | - `yarn test` - run tests 29 | - `yarn coverage` - generate test coverage report 30 | -------------------------------------------------------------------------------- /contracts/InstantUniswapPrice.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | import "@openzeppelin/contracts/math/SafeMath.sol"; 6 | import "./Uniswap/UniswapV2Library.sol"; 7 | import "./interfaces/IUniswapV2Pair.sol"; 8 | import "./interfaces/IUniswapV2Factory.sol"; 9 | import "./interfaces/BPoolInterface.sol"; 10 | import "./interfaces/IERC20Detailed.sol"; 11 | 12 | contract InstantUniswapPrice { 13 | using SafeMath for uint256; 14 | 15 | address public constant WETH_TOKEN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 16 | address public constant USDC_MARKET = 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc; 17 | address public constant UNISWAP_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; 18 | 19 | function contractUsdTokensSum(address _contract, address[] memory _tokens) public view returns (uint256) { 20 | uint256[] memory balances = getContractTokensBalanceOfArray(_contract, _tokens); 21 | return usdcTokensSum(_tokens, balances); 22 | } 23 | 24 | function contractEthTokensSum(address _contract, address[] memory _tokens) public view returns (uint256) { 25 | uint256[] memory balances = getContractTokensBalanceOfArray(_contract, _tokens); 26 | return ethTokensSum(_tokens, balances); 27 | } 28 | 29 | function balancerPoolUsdTokensSum(address _balancerPool) public view returns (uint256) { 30 | (address[] memory tokens, uint256[] memory balances) = getBalancerTokensAndBalances(_balancerPool); 31 | return usdcTokensSum(tokens, balances); 32 | } 33 | 34 | function balancerPoolEthTokensSum(address _balancerPool) public view returns (uint256) { 35 | (address[] memory tokens, uint256[] memory balances) = getBalancerTokensAndBalances(_balancerPool); 36 | return ethTokensSum(tokens, balances); 37 | } 38 | 39 | function usdcTokensSum(address[] memory _tokens, uint256[] memory _balances) public view returns (uint256) { 40 | uint256 ethTokensSumAmount = ethTokensSum(_tokens, _balances); 41 | uint256 ethPriceInUsdc = currentEthPriceInUsdc(); 42 | return ethTokensSumAmount.mul(ethPriceInUsdc).div(1 ether); 43 | } 44 | 45 | function ethTokensSum(address[] memory _tokens, uint256[] memory _balances) public view returns (uint256) { 46 | uint256 len = _tokens.length; 47 | require(len == _balances.length, "LENGTHS_NOT_EQUAL"); 48 | 49 | uint256 sum = 0; 50 | for (uint256 i = 0; i < len; i++) { 51 | _balances[i] = amountToEther(_balances[i], getTokenDecimals(_tokens[i])); 52 | sum = sum.add(currentTokenEthPrice(_tokens[i]).mul(_balances[i]).div(1 ether)); 53 | } 54 | return sum; 55 | } 56 | 57 | function currentEthPriceInUsdc() public view returns (uint256) { 58 | return currentTokenPrice(USDC_MARKET, WETH_TOKEN); 59 | } 60 | 61 | function currentTokenUsdcPrice(address _token) public view returns (uint256 price) { 62 | uint256 ethPriceInUsdc = currentEthPriceInUsdc(); 63 | uint256 tokenEthPrice = currentTokenEthPrice(_token); 64 | return tokenEthPrice.mul(ethPriceInUsdc).div(1 ether); 65 | } 66 | 67 | function currentTokenEthPrice(address _token) public view returns (uint256 price) { 68 | if (_token == WETH_TOKEN) { 69 | return uint256(1 ether); 70 | } 71 | address market = IUniswapV2Factory(UNISWAP_FACTORY).getPair(_token, WETH_TOKEN); 72 | if (market == address(0)) { 73 | market = IUniswapV2Factory(UNISWAP_FACTORY).getPair(WETH_TOKEN, _token); 74 | return currentTokenPrice(market, _token); 75 | } else { 76 | return currentTokenPrice(market, _token); 77 | } 78 | } 79 | 80 | function currentTokenPrice(address uniswapMarket, address _token) public view returns (uint256 price) { 81 | (uint112 reserve0, uint112 reserve1, ) = IUniswapV2Pair(uniswapMarket).getReserves(); 82 | address token0 = IUniswapV2Pair(uniswapMarket).token0(); 83 | address token1 = IUniswapV2Pair(uniswapMarket).token1(); 84 | 85 | uint8 tokenInDecimals = getTokenDecimals(_token); 86 | uint8 tokenOutDecimals = getTokenDecimals(_token == token0 ? token1 : token0); 87 | 88 | uint256 inAmount = 1 ether; 89 | if (tokenInDecimals < uint8(18)) { 90 | inAmount = inAmount.div(10**uint256(uint8(18) - tokenInDecimals)); 91 | } 92 | 93 | price = UniswapV2Library.getAmountOut( 94 | inAmount, 95 | _token == token0 ? reserve0 : reserve1, 96 | _token == token0 ? reserve1 : reserve0 97 | ); 98 | 99 | if (tokenInDecimals > tokenOutDecimals) { 100 | return price.mul(10**uint256(tokenInDecimals - tokenOutDecimals)); 101 | } else { 102 | return price; 103 | } 104 | } 105 | 106 | function getBalancerTokensAndBalances(address _balancerPool) 107 | public 108 | view 109 | returns (address[] memory tokens, uint256[] memory balances) 110 | { 111 | tokens = BPoolInterface(_balancerPool).getCurrentTokens(); 112 | uint256 len = tokens.length; 113 | 114 | balances = new uint256[](len); 115 | for (uint256 i = 0; i < len; i++) { 116 | balances[i] = BPoolInterface(_balancerPool).getBalance(tokens[i]); 117 | } 118 | } 119 | 120 | function getContractTokensBalanceOfArray(address _contract, address[] memory tokens) 121 | public 122 | view 123 | returns (uint256[] memory balances) 124 | { 125 | uint256 len = tokens.length; 126 | balances = new uint256[](len); 127 | for (uint256 i = 0; i < len; i++) { 128 | balances[i] = IERC20Detailed(tokens[i]).balanceOf(_contract); 129 | } 130 | } 131 | 132 | function getTokenDecimals(address _token) public view returns (uint8 decimals) { 133 | try IERC20Detailed(_token).decimals() returns (uint8 _decimals) { 134 | decimals = _decimals; 135 | } catch ( 136 | bytes memory /*lowLevelData*/ 137 | ) { 138 | decimals = uint8(18); 139 | } 140 | } 141 | 142 | function amountToEther(uint256 amount, uint8 decimals) public pure returns (uint256) { 143 | if (decimals == uint8(18)) { 144 | return amount; 145 | } 146 | return amount.mul(10**uint256(uint8(18) - decimals)); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /contracts/PowerOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "@openzeppelin/upgrades-core/contracts/Initializable.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/math/SafeMath.sol"; 9 | import "@openzeppelin/contracts/utils/SafeCast.sol"; 10 | import "./interfaces/IPowerOracleV3.sol"; 11 | import "./interfaces/IPowerPoke.sol"; 12 | import "./UniswapTWAPProvider.sol"; 13 | import "./utils/PowerPausable.sol"; 14 | import "./utils/PowerOwnable.sol"; 15 | import "./PowerPoke.sol"; 16 | import "./PowerOracleTokenManagement.sol"; 17 | 18 | contract PowerOracle is 19 | IPowerOracle, 20 | PowerOwnable, 21 | Initializable, 22 | PowerPausable, 23 | PowerOracleTokenManagement, 24 | UniswapTWAPProvider 25 | { 26 | using SafeMath for uint256; 27 | using SafeCast for uint256; 28 | 29 | uint256 public constant POWER_ORACLE_VERSION = 3; 30 | 31 | uint256 internal constant COMPENSATION_PLAN_1_ID = 1; 32 | uint256 internal constant COMPENSATION_PLAN_2_ID = 2; 33 | uint256 public constant HUNDRED_PCT = 100 ether; 34 | 35 | /// @notice The event emitted when a reporter calls a poke operation 36 | event PokeFromReporter(uint256 indexed reporterId, uint256 tokenCount); 37 | 38 | /// @notice The event emitted when a slasher executes poke and slashes the current reporter 39 | event PokeFromSlasher(uint256 indexed slasherId, uint256 tokenCount); 40 | 41 | /// @notice The event emitted when an arbitrary user calls poke operation 42 | event Poke(address indexed poker, uint256 tokenCount); 43 | 44 | /// @notice The event emitted when the owner updates the powerOracleStaking address 45 | event SetPowerPoke(address powerPoke); 46 | 47 | /// @notice The event emitted when the slasher timestamps are updated 48 | event SlasherHeartbeat(uint256 indexed slasherId, uint256 prevSlasherTimestamp, uint256 newSlasherTimestamp); 49 | 50 | /// @notice CVP token address 51 | IERC20 public immutable CVP_TOKEN; 52 | 53 | modifier onlyReporter(uint256 reporterId_, bytes calldata rewardOpts) { 54 | uint256 gasStart = gasleft(); 55 | powerPoke.authorizeReporter(reporterId_, msg.sender); 56 | _; 57 | powerPoke.reward(reporterId_, gasStart.sub(gasleft()), COMPENSATION_PLAN_1_ID, rewardOpts); 58 | } 59 | 60 | modifier onlySlasher(uint256 slasherId_, bytes calldata rewardOpts) { 61 | uint256 gasStart = gasleft(); 62 | powerPoke.authorizeNonReporter(slasherId_, msg.sender); 63 | _; 64 | powerPoke.reward(slasherId_, gasStart.sub(gasleft()), COMPENSATION_PLAN_1_ID, rewardOpts); 65 | } 66 | 67 | modifier onlyEOA() { 68 | require(msg.sender == tx.origin, "CONTRACT_CALL"); 69 | _; 70 | } 71 | 72 | constructor(address cvpToken_, uint256 anchorPeriod_) public UniswapTWAPProvider(anchorPeriod_) { 73 | CVP_TOKEN = IERC20(cvpToken_); 74 | } 75 | 76 | function initialize(address owner_, address powerPoke_) external initializer { 77 | _transferOwnership(owner_); 78 | powerPoke = IPowerPoke(powerPoke_); 79 | } 80 | 81 | /*** Current Poke Interface ***/ 82 | 83 | function _fetchEthPrice() internal returns (uint256) { 84 | bytes32 symbolHash = keccak256(abi.encodePacked("ETH")); 85 | if (getIntervalStatus(symbolHash) == ReportInterval.LESS_THAN_MIN) { 86 | return uint256(prices[symbolHash].value); 87 | } 88 | uint256 ethPrice = fetchEthPrice(); 89 | _savePrice(symbolHash, ethPrice); 90 | return ethPrice; 91 | } 92 | 93 | function _fetchCvpPrice(uint256 ethPrice_) internal returns (uint256) { 94 | bytes32 symbolHash = keccak256(abi.encodePacked("CVP")); 95 | if (getIntervalStatus(symbolHash) == ReportInterval.LESS_THAN_MIN) { 96 | return uint256(prices[symbolHash].value); 97 | } 98 | uint256 cvpPrice = fetchCvpPrice(ethPrice_); 99 | _savePrice(symbolHash, cvpPrice); 100 | return cvpPrice; 101 | } 102 | 103 | function _fetchAndSavePrice( 104 | string memory symbol_, 105 | uint256 ethPrice_, 106 | uint256 minReportInterval_, 107 | uint256 maxReportInterval_ 108 | ) internal returns (ReportInterval) { 109 | address token = tokenBySymbol[symbol_]; 110 | TokenConfig memory basicConfig = getActiveTokenConfig(token); 111 | TokenConfigUpdate memory updateConfig = getTokenUpdateConfig(token); 112 | 113 | require(basicConfig.priceSource == PRICE_SOURCE_REPORTER, "NOT_REPORTED_PRICE_SOURCE"); 114 | bytes32 symbolHash = keccak256(abi.encodePacked(symbol_)); 115 | 116 | ReportInterval intervalStatus = getIntervalStatusForIntervals(symbolHash, minReportInterval_, maxReportInterval_); 117 | if (intervalStatus == ReportInterval.LESS_THAN_MIN) { 118 | return intervalStatus; 119 | } 120 | 121 | uint256 price; 122 | if (symbolHash == ethHash) { 123 | price = ethPrice_; 124 | } else { 125 | price = fetchAnchorPrice(symbol_, basicConfig, updateConfig, ethPrice_); 126 | } 127 | 128 | _savePrice(symbolHash, price); 129 | 130 | return intervalStatus; 131 | } 132 | 133 | function _savePrice(bytes32 _symbolHash, uint256 price_) internal { 134 | prices[_symbolHash] = Price(block.timestamp.toUint128(), price_.toUint128()); 135 | } 136 | 137 | /*** Pokers ***/ 138 | 139 | /** 140 | * @notice A reporter pokes symbols with incentive to be rewarded 141 | * @param reporterId_ The valid reporter's user ID 142 | * @param symbols_ Asset symbols to poke 143 | */ 144 | function pokeFromReporter( 145 | uint256 reporterId_, 146 | string[] memory symbols_, 147 | bytes calldata rewardOpts_ 148 | ) external override onlyReporter(reporterId_, rewardOpts_) onlyEOA whenNotPaused { 149 | uint256 len = symbols_.length; 150 | require(len > 0, "MISSING_SYMBOLS"); 151 | 152 | uint256 ethPrice = _fetchEthPrice(); 153 | _fetchCvpPrice(ethPrice); 154 | 155 | (uint256 minReportInterval, uint256 maxReportInterval) = _getMinMaxReportInterval(); 156 | 157 | for (uint256 i = 0; i < len; i++) { 158 | require( 159 | _fetchAndSavePrice(symbols_[i], ethPrice, minReportInterval, maxReportInterval) != ReportInterval.LESS_THAN_MIN, 160 | "TOO_EARLY_UPDATE" 161 | ); 162 | } 163 | 164 | emit PokeFromReporter(reporterId_, len); 165 | } 166 | 167 | /** 168 | * @notice A slasher pokes symbols with incentive to be rewarded 169 | * @param slasherId_ The slasher's user ID 170 | * @param symbols_ Asset symbols to poke 171 | */ 172 | function pokeFromSlasher( 173 | uint256 slasherId_, 174 | string[] memory symbols_, 175 | bytes calldata rewardOpts_ 176 | ) external override onlySlasher(slasherId_, rewardOpts_) onlyEOA whenNotPaused { 177 | uint256 len = symbols_.length; 178 | require(len > 0, "MISSING_SYMBOLS"); 179 | 180 | uint256 ethPrice = _fetchEthPrice(); 181 | _fetchCvpPrice(ethPrice); 182 | 183 | (uint256 minReportInterval, uint256 maxReportInterval) = _getMinMaxReportInterval(); 184 | 185 | for (uint256 i = 0; i < len; i++) { 186 | require( 187 | _fetchAndSavePrice(symbols_[i], ethPrice, minReportInterval, maxReportInterval) == 188 | ReportInterval.GREATER_THAN_MAX, 189 | "INTERVAL_IS_OK" 190 | ); 191 | } 192 | 193 | _updateSlasherTimestamp(slasherId_, false); 194 | powerPoke.slashReporter(slasherId_, len); 195 | 196 | emit PokeFromSlasher(slasherId_, len); 197 | } 198 | 199 | function slasherHeartbeat(uint256 slasherId_) external override whenNotPaused onlyEOA { 200 | uint256 gasStart = gasleft(); 201 | powerPoke.authorizeNonReporter(slasherId_, msg.sender); 202 | 203 | _updateSlasherTimestamp(slasherId_, true); 204 | 205 | PowerPoke.PokeRewardOptions memory opts = PowerPoke.PokeRewardOptions(msg.sender, false); 206 | bytes memory rewardConfig = abi.encode(opts); 207 | // reward in CVP 208 | powerPoke.reward(slasherId_, gasStart.sub(gasleft()), COMPENSATION_PLAN_2_ID, rewardConfig); 209 | } 210 | 211 | function _updateSlasherTimestamp(uint256 _slasherId, bool assertOnTimeDelta) internal { 212 | uint256 prevSlasherUpdate = lastSlasherUpdates[_slasherId]; 213 | 214 | if (assertOnTimeDelta) { 215 | uint256 delta = block.timestamp.sub(prevSlasherUpdate); 216 | require(delta >= powerPoke.getSlasherHeartbeat(address(this)), "BELOW_HEARTBEAT_INTERVAL"); 217 | } 218 | 219 | lastSlasherUpdates[_slasherId] = block.timestamp; 220 | emit SlasherHeartbeat(_slasherId, prevSlasherUpdate, block.timestamp); 221 | } 222 | 223 | /** 224 | * @notice Arbitrary user pokes symbols without being rewarded 225 | * @param symbols_ Asset symbols to poke 226 | */ 227 | function poke(string[] memory symbols_) external override whenNotPaused { 228 | uint256 len = symbols_.length; 229 | require(len > 0, "MISSING_SYMBOLS"); 230 | 231 | uint256 ethPrice = _fetchEthPrice(); 232 | (uint256 minReportInterval, uint256 maxReportInterval) = _getMinMaxReportInterval(); 233 | 234 | for (uint256 i = 0; i < len; i++) { 235 | _fetchAndSavePrice(symbols_[i], ethPrice, minReportInterval, maxReportInterval); 236 | } 237 | 238 | emit Poke(msg.sender, len); 239 | } 240 | 241 | /*** Owner Interface ***/ 242 | 243 | /** 244 | * @notice The owner sets a new powerPoke contract 245 | * @param powerPoke_ The powerPoke contract address 246 | */ 247 | function setPowerPoke(address powerPoke_) external override onlyOwner { 248 | powerPoke = PowerPoke(powerPoke_); 249 | emit SetPowerPoke(powerPoke_); 250 | } 251 | 252 | /** 253 | * @notice The owner pauses poke*-operations 254 | */ 255 | function pause() external override onlyOwner { 256 | _pause(); 257 | } 258 | 259 | /** 260 | * @notice The owner unpauses poke*-operations 261 | */ 262 | function unpause() external override onlyOwner { 263 | _unpause(); 264 | } 265 | 266 | /*** Viewers ***/ 267 | 268 | function _getMinMaxReportInterval() internal view returns (uint256 min, uint256 max) { 269 | return powerPoke.getMinMaxReportIntervals(address(this)); 270 | } 271 | 272 | function getIntervalStatus(bytes32 _symbolHash) public view returns (ReportInterval) { 273 | (uint256 minReportInterval, uint256 maxReportInterval) = _getMinMaxReportInterval(); 274 | 275 | require(minReportInterval > 0 && maxReportInterval > 0, "0_INTERVAL"); 276 | 277 | return getIntervalStatusForIntervals(_symbolHash, minReportInterval, maxReportInterval); 278 | } 279 | 280 | function getIntervalStatusForIntervals( 281 | bytes32 symbolHash_, 282 | uint256 minReportInterval_, 283 | uint256 maxReportInterval_ 284 | ) public view returns (ReportInterval) { 285 | uint256 delta = block.timestamp.sub(prices[symbolHash_].timestamp); 286 | 287 | if (delta < minReportInterval_) { 288 | return ReportInterval.LESS_THAN_MIN; 289 | } 290 | 291 | if (delta < maxReportInterval_) { 292 | return ReportInterval.OK; 293 | } 294 | 295 | return ReportInterval.GREATER_THAN_MAX; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /contracts/PowerOracleReader.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | import "@openzeppelin/contracts/math/SafeMath.sol"; 6 | import "./interfaces/IPowerOracleV3Reader.sol"; 7 | import "./PowerOracleTokenManagement.sol"; 8 | pragma experimental ABIEncoderV2; 9 | 10 | contract PowerOracleReader is IPowerOracleV3Reader, PowerOracleTokenManagement { 11 | using SafeMath for uint256; 12 | 13 | /// @notice The number of wei in 1 ETH 14 | uint256 public constant ethBaseUnit = 1e18; 15 | 16 | bytes32 internal constant cvpHash = keccak256(abi.encodePacked("CVP")); 17 | bytes32 internal constant ethHash = keccak256(abi.encodePacked("ETH")); 18 | 19 | uint256 internal constant PRICE_SOURCE_FIXED_USD = 0; 20 | uint256 internal constant PRICE_SOURCE_REPORTER = 1; 21 | 22 | function priceInternal(TokenConfig memory config_) internal view returns (uint256) { 23 | if (config_.priceSource == PRICE_SOURCE_REPORTER) return prices[config_.symbolHash].value; 24 | if (config_.priceSource == PRICE_SOURCE_FIXED_USD) return uint256(config_.fixedPrice); 25 | revert("UNSUPPORTED_PRICE_CASE"); 26 | } 27 | 28 | /** 29 | * @notice Get the underlying price of a token 30 | * @param token_ The token address for price retrieval 31 | * @return Price denominated in USD, with 6 decimals, for the given asset address 32 | */ 33 | function getPriceByAsset(address token_) external view override returns (uint256) { 34 | TokenConfig memory config = getActiveTokenConfig(token_); 35 | return priceInternal(config); 36 | } 37 | 38 | /** 39 | * @notice Get the official price for a symbol, like "COMP" 40 | * @param symbol_ The symbol for price retrieval 41 | * @return Price denominated in USD, with 6 decimals 42 | */ 43 | function getPriceBySymbol(string calldata symbol_) external view override returns (uint256) { 44 | TokenConfig memory config = getTokenConfigBySymbol(symbol_); 45 | return priceInternal(config); 46 | } 47 | 48 | /** 49 | * @notice Get price by a token symbol hash, 50 | * like "0xd6aca1be9729c13d677335161321649cccae6a591554772516700f986f942eaa" for USDC 51 | * @param symbolHash_ The symbol hash for price retrieval 52 | * @return Price denominated in USD, with 6 decimals, for the given asset address 53 | */ 54 | function getPriceBySymbolHash(bytes32 symbolHash_) external view override returns (uint256) { 55 | TokenConfig memory config = getTokenConfigBySymbolHash(symbolHash_); 56 | return priceInternal(config); 57 | } 58 | 59 | /** 60 | * @notice Get the underlying price of multiple tokens 61 | * @param tokens_ The token addresses for price retrieval 62 | * @return Price denominated in USD, with 6 decimals, for a given asset address 63 | */ 64 | function getAssetPrices(address[] calldata tokens_) external view override returns (uint256[] memory) { 65 | uint256 len = tokens_.length; 66 | uint256[] memory result = new uint256[](len); 67 | 68 | for (uint256 i = 0; i < len; i++) { 69 | TokenConfig memory config = getActiveTokenConfig(tokens_[i]); 70 | result[i] = priceInternal(config); 71 | } 72 | 73 | return result; 74 | } 75 | 76 | /** 77 | * @notice Get the underlying price of multiple tokens 78 | * @param tokens_ The token addresses for price retrieval 79 | * @return Price denominated in USD, with 18 decimals, for a given asset address 80 | */ 81 | function getAssetPrices18(address[] calldata tokens_) external view override returns (uint256[] memory) { 82 | uint256 len = tokens_.length; 83 | uint256[] memory result = new uint256[](len); 84 | 85 | for (uint256 i = 0; i < len; i++) { 86 | TokenConfig memory config = getActiveTokenConfig(tokens_[i]); 87 | result[i] = priceInternal(config).mul(1e12); 88 | } 89 | 90 | return result; 91 | } 92 | 93 | /** 94 | * @notice Get the underlying price of a token 95 | * @param token_ The token address for price retrieval 96 | * @return Price denominated in USD, with 18 decimals, for the given asset address 97 | */ 98 | function getPriceByAsset18(address token_) external view override returns (uint256) { 99 | TokenConfig memory config = getActiveTokenConfig(token_); 100 | return priceInternal(config).mul(1e12); 101 | } 102 | 103 | /** 104 | * @notice Get the official price for a symbol, like "COMP" 105 | * @param symbol_ The symbol for price retrieval 106 | * @return Price denominated in USD, with 18 decimals 107 | */ 108 | function getPriceBySymbol18(string calldata symbol_) external view override returns (uint256) { 109 | TokenConfig memory config = getTokenConfigBySymbol(symbol_); 110 | return priceInternal(config).mul(1e12); 111 | } 112 | 113 | /** 114 | * @notice Get price by a token symbol hash, 115 | * like "0xd6aca1be9729c13d677335161321649cccae6a591554772516700f986f942eaa" for USDC 116 | * @param symbolHash_ The symbol hash for price retrieval 117 | * @return Price denominated in USD, with 18 decimals, for the given asset address 118 | */ 119 | function getPriceBySymbolHash18(bytes32 symbolHash_) external view override returns (uint256) { 120 | TokenConfig memory config = getTokenConfigBySymbolHash(symbolHash_); 121 | return priceInternal(config).mul(1e12); 122 | } 123 | 124 | /** 125 | * @notice Get the price by underlying address 126 | * @dev Implements the old PriceOracle interface for Compound v2. 127 | * @param token_ The underlying address for price retrieval 128 | * @return Price denominated in USD, with 18 decimals, for the given underlying address 129 | */ 130 | function assetPrices(address token_) external view override returns (uint256) { 131 | TokenConfig memory config = getActiveTokenConfig(token_); 132 | // Return price in the same format as getUnderlyingPrice, but by token address 133 | return priceInternal(config).mul(1e12); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /contracts/PowerOracleStorageV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "./interfaces/IPowerPoke.sol"; 7 | 8 | contract PowerOracleStorageV1 { 9 | struct Price { 10 | uint128 timestamp; 11 | uint128 value; 12 | } 13 | 14 | struct Observation { 15 | uint256 timestamp; 16 | uint256 acc; 17 | } 18 | 19 | struct TokenConfig { 20 | // Slot #1 21 | uint96 baseUnit; 22 | uint96 fixedPrice; 23 | uint8 priceSource; 24 | uint8 active; 25 | // Slot #2 26 | bytes32 symbolHash; 27 | } 28 | 29 | struct TokenConfigUpdate { 30 | address uniswapMarket; 31 | bool isUniswapReversed; 32 | } 33 | 34 | /// @notice The linked PowerOracleStaking contract address 35 | IPowerPoke public powerPoke; 36 | 37 | /// @notice Official reported prices and timestamps by symbol hash 38 | mapping(bytes32 => Price) public prices; 39 | 40 | /// @notice Last slasher update time by a user ID 41 | mapping(uint256 => uint256) public lastSlasherUpdates; 42 | 43 | /// @notice The old observation for each symbolHash 44 | mapping(bytes32 => Observation) public oldObservations; 45 | 46 | /// @notice The new observation for each symbolHash 47 | mapping(bytes32 => Observation) public newObservations; 48 | 49 | address[] public tokens; 50 | 51 | mapping(string => address) public tokenBySymbol; 52 | mapping(bytes32 => address) public tokenBySymbolHash; 53 | 54 | // token => TokenConfig, push-only 55 | mapping(address => TokenConfig) internal tokenConfigs; 56 | 57 | // token => TokenUpdateConfig, push-only 58 | mapping(address => TokenConfigUpdate) internal tokenUpdateConfigs; 59 | } 60 | -------------------------------------------------------------------------------- /contracts/PowerOracleTokenManagement.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "./interfaces/IPowerOracleV3TokenManagement.sol"; 7 | import "./PowerOracleStorageV1.sol"; 8 | import "./utils/PowerOwnable.sol"; 9 | 10 | abstract contract PowerOracleTokenManagement is IPowerOracleV3TokenManagement, PowerOwnable, PowerOracleStorageV1 { 11 | uint8 internal constant TOKEN_ACTIVITY_NOT_EXISTS = 0; 12 | uint8 internal constant TOKEN_ACTIVITY_DEPRECATED = 1; 13 | uint8 internal constant TOKEN_ACTIVITY_ACTIVE = 2; 14 | 15 | event AddToken( 16 | address indexed token, 17 | bytes32 indexed symbolHash, 18 | string symbol, 19 | uint96 baseUnit, 20 | uint96 fixedPrice, 21 | uint8 priceSource, 22 | address uniswapMarket, 23 | bool isUniswapReversed 24 | ); 25 | event UpdateTokenMarket(address indexed token, address indexed uniswapMarket, bool isUniswapReversed); 26 | event SetTokenActivity(address indexed token, uint8 active); 27 | 28 | /// @notice Required only for the setup function, not persisted in the storage 29 | struct TokenConfigSetup { 30 | address token; 31 | string symbol; 32 | TokenConfig basic; 33 | TokenConfigUpdate update; 34 | } 35 | 36 | /// @notice Required only for the setup function, not persisted in the storage 37 | struct TokenConfigUpdateSetup { 38 | address token; 39 | TokenConfigUpdate update; 40 | } 41 | 42 | /// @notice Required only for the setup function, not persisted in the storage 43 | struct TokenActivitySetup { 44 | address token; 45 | uint8 active; 46 | } 47 | 48 | function addTokens(TokenConfigSetup[] memory setup_) external onlyOwner { 49 | uint256 len = setup_.length; 50 | 51 | for (uint256 i = 0; i < len; i++) { 52 | TokenConfigSetup memory tc = setup_[i]; 53 | address token = tc.token; 54 | 55 | require(tc.basic.symbolHash == keccak256(abi.encodePacked(tc.symbol)), "INVALID_SYMBOL_HASH"); 56 | require(tokenConfigs[token].active == TOKEN_ACTIVITY_NOT_EXISTS, "ALREADY_EXISTS"); 57 | require(tc.basic.baseUnit > 0, "BASE_UNIT_IS_NULL"); 58 | require(tc.basic.active > 0 && tc.basic.active <= 2, "INVALID_ACTIVITY_STATUS"); 59 | require(tc.basic.priceSource <= 1, "INVALID_PRICE_SOURCE"); 60 | require(tokenBySymbolHash[tc.basic.symbolHash] == address(0), "TOKEN_SYMBOL_ALREADY_MAPPED"); 61 | 62 | tokenConfigs[token] = setup_[i].basic; 63 | tokenUpdateConfigs[token] = setup_[i].update; 64 | 65 | tokenBySymbol[tc.symbol] = token; 66 | tokenBySymbolHash[tc.basic.symbolHash] = token; 67 | 68 | tokens.push(token); 69 | 70 | emit AddToken( 71 | token, 72 | tc.basic.symbolHash, 73 | tc.symbol, 74 | tc.basic.baseUnit, 75 | tc.basic.fixedPrice, 76 | tc.basic.priceSource, 77 | tc.update.uniswapMarket, 78 | tc.update.isUniswapReversed 79 | ); 80 | } 81 | } 82 | 83 | function updateTokenMarket(TokenConfigUpdateSetup[] memory setup_) external onlyOwner { 84 | uint256 len = setup_.length; 85 | 86 | for (uint256 i = 0; i < len; i++) { 87 | address token = setup_[i].token; 88 | require(tokenConfigs[token].active > 0 && tokenConfigs[token].active <= 2, "INVALID_ACTIVITY_STATUS"); 89 | 90 | tokenUpdateConfigs[token] = setup_[i].update; 91 | emit UpdateTokenMarket(token, setup_[i].update.uniswapMarket, setup_[i].update.isUniswapReversed); 92 | } 93 | } 94 | 95 | function setTokenActivities(TokenActivitySetup[] calldata setup_) external onlyOwner { 96 | uint256 len = setup_.length; 97 | 98 | for (uint256 i = 0; i < len; i++) { 99 | address token = setup_[i].token; 100 | uint8 tokenActivity = setup_[i].active; 101 | 102 | require(tokenConfigs[token].active > 0, "INVALID_CURRENT_ACTIVITY_STATUS"); 103 | require(tokenActivity > 0 && tokenActivity <= 2, "INVALID_NEW_ACTIVITY_STATUS"); 104 | 105 | tokenConfigs[token].active = tokenActivity; 106 | emit SetTokenActivity(token, tokenActivity); 107 | } 108 | } 109 | 110 | function getTokenUpdateConfig(address token_) public view returns (TokenConfigUpdate memory) { 111 | return tokenUpdateConfigs[token_]; 112 | } 113 | 114 | function getTokenConfig(address token_) public view returns (TokenConfig memory) { 115 | return tokenConfigs[token_]; 116 | } 117 | 118 | function getActiveTokenConfig(address token_) public view returns (TokenConfig memory) { 119 | TokenConfig memory cfg = tokenConfigs[token_]; 120 | require(token_ != address(0) && cfg.active == TOKEN_ACTIVITY_ACTIVE, "TOKEN_NOT_FOUND"); 121 | return cfg; 122 | } 123 | 124 | function getTokenConfigBySymbolHash(bytes32 symbolHash_) public view returns (TokenConfig memory) { 125 | return getActiveTokenConfig(tokenBySymbolHash[symbolHash_]); 126 | } 127 | 128 | function getTokenConfigBySymbol(string memory symbol_) public view returns (TokenConfig memory) { 129 | return getActiveTokenConfig(tokenBySymbol[symbol_]); 130 | } 131 | 132 | function getTokens() external view override returns (address[] memory) { 133 | return tokens; 134 | } 135 | 136 | function getTokenCount() external view override returns (uint256) { 137 | return tokens.length; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /contracts/PowerPoke.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "@openzeppelin/upgrades-core/contracts/Initializable.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; 9 | import "@openzeppelin/contracts/math/SafeMath.sol"; 10 | import "@openzeppelin/contracts/math/Math.sol"; 11 | import "./interfaces/IPowerOracleV3Reader.sol"; 12 | import "./interfaces/IUniswapV2Router02.sol"; 13 | import "./interfaces/IEACAggregatorProxy.sol"; 14 | import "./interfaces/IPowerPoke.sol"; 15 | import "./utils/PowerOwnable.sol"; 16 | import "./utils/PowerPausable.sol"; 17 | import "./PowerPokeStaking.sol"; 18 | import "./PowerPokeStorageV1.sol"; 19 | 20 | contract PowerPoke is IPowerPoke, PowerOwnable, Initializable, PowerPausable, ReentrancyGuard, PowerPokeStorageV1 { 21 | using SafeMath for uint256; 22 | 23 | event RewardUser( 24 | address indexed client, 25 | uint256 indexed userId, 26 | uint256 indexed bonusPlan, 27 | bool compensateInETH, 28 | uint256 gasUsed, 29 | uint256 gasPrice, 30 | uint256 userDeposit, 31 | uint256 ethPrice, 32 | uint256 cvpPrice, 33 | uint256 compensationEvaluationCVP, 34 | uint256 bonusCVP, 35 | uint256 earnedCVP, 36 | uint256 earnedETH 37 | ); 38 | 39 | event TransferClientOwnership(address indexed client, address indexed from, address indexed to); 40 | 41 | event SetReportIntervals(address indexed client, uint256 minReportInterval, uint256 maxReportInterval); 42 | 43 | event SetGasPriceLimit(address indexed client, uint256 gasPriceLimit); 44 | 45 | event SetSlasherHeartbeat(address indexed client, uint256 slasherHeartbeat); 46 | 47 | event SetBonusPlan( 48 | address indexed client, 49 | uint256 indexed planId, 50 | bool indexed active, 51 | uint64 bonusNominator, 52 | uint64 bonsuDenominator, 53 | uint128 perGas 54 | ); 55 | 56 | event SetFixedCompensations(address indexed client, uint256 fixedCompensationETH, uint256 fixedCompensationCVP); 57 | 58 | event SetDefaultMinDeposit(address indexed client, uint256 defaultMinDeposit); 59 | 60 | event WithdrawRewards(uint256 indexed userId, address indexed to, uint256 amount); 61 | 62 | event AddCredit(address indexed client, uint256 amount); 63 | 64 | event WithdrawCredit(address indexed client, address indexed to, uint256 amount); 65 | 66 | event SetOracle(address indexed oracle); 67 | 68 | event AddClient( 69 | address indexed client, 70 | address indexed owner, 71 | bool canSlash, 72 | uint256 gasPriceLimit, 73 | uint256 minReportInterval, 74 | uint256 maxReportInterval, 75 | uint256 slasherHeartbeat 76 | ); 77 | 78 | event SetClientActiveFlag(address indexed client, bool indexed active); 79 | 80 | event SetCanSlashFlag(address indexed client, bool indexed canSlash); 81 | 82 | event SetPokerKeyRewardWithdrawAllowance(uint256 indexed userId, bool allow); 83 | 84 | struct PokeRewardOptions { 85 | address to; 86 | bool compensateInETH; 87 | } 88 | 89 | struct RewardHelperStruct { 90 | uint256 gasPrice; 91 | uint256 ethPrice; 92 | uint256 cvpPrice; 93 | uint256 totalInCVP; 94 | uint256 compensationCVP; 95 | uint256 bonusCVP; 96 | uint256 earnedCVP; 97 | uint256 earnedETH; 98 | } 99 | 100 | address public immutable WETH_TOKEN; 101 | 102 | IERC20 public immutable CVP_TOKEN; 103 | 104 | IEACAggregatorProxy public immutable FAST_GAS_ORACLE; 105 | 106 | PowerPokeStaking public immutable POWER_POKE_STAKING; 107 | 108 | IUniswapV2Router02 public immutable UNISWAP_ROUTER; 109 | 110 | modifier onlyClientOwner(address client_) { 111 | require(clients[client_].owner == msg.sender, "ONLY_CLIENT_OWNER"); 112 | _; 113 | } 114 | 115 | constructor( 116 | address cvpToken_, 117 | address wethToken_, 118 | address fastGasOracle_, 119 | address uniswapRouter_, 120 | address powerPokeStaking_ 121 | ) public { 122 | require(cvpToken_ != address(0), "CVP_ADDR_IS_0"); 123 | require(wethToken_ != address(0), "WETH_ADDR_IS_0"); 124 | require(fastGasOracle_ != address(0), "FAST_GAS_ORACLE_IS_0"); 125 | require(uniswapRouter_ != address(0), "UNISWAP_ROUTER_IS_0"); 126 | require(powerPokeStaking_ != address(0), "POWER_POKE_STAKING_ADDR_IS_0"); 127 | 128 | CVP_TOKEN = IERC20(cvpToken_); 129 | WETH_TOKEN = wethToken_; 130 | FAST_GAS_ORACLE = IEACAggregatorProxy(fastGasOracle_); 131 | POWER_POKE_STAKING = PowerPokeStaking(powerPokeStaking_); 132 | UNISWAP_ROUTER = IUniswapV2Router02(uniswapRouter_); 133 | } 134 | 135 | function initialize(address owner_, address oracle_) external initializer { 136 | _transferOwnership(owner_); 137 | oracle = oracle_; 138 | } 139 | 140 | /*** CLIENT'S CONTRACT INTERFACE ***/ 141 | function authorizeReporter(uint256 userId_, address pokerKey_) external view override { 142 | POWER_POKE_STAKING.authorizeHDH(userId_, pokerKey_); 143 | } 144 | 145 | function authorizeNonReporter(uint256 userId_, address pokerKey_) external view override { 146 | POWER_POKE_STAKING.authorizeNonHDH(userId_, pokerKey_, clients[msg.sender].defaultMinDeposit); 147 | } 148 | 149 | function authorizeNonReporterWithDeposit( 150 | uint256 userId_, 151 | address pokerKey_, 152 | uint256 overrideMinDeposit_ 153 | ) external view override { 154 | POWER_POKE_STAKING.authorizeNonHDH(userId_, pokerKey_, overrideMinDeposit_); 155 | } 156 | 157 | function authorizePoker(uint256 userId_, address pokerKey_) external view override { 158 | POWER_POKE_STAKING.authorizeMember(userId_, pokerKey_, clients[msg.sender].defaultMinDeposit); 159 | } 160 | 161 | function authorizePokerWithDeposit( 162 | uint256 userId_, 163 | address pokerKey_, 164 | uint256 overrideMinStake_ 165 | ) external view override { 166 | POWER_POKE_STAKING.authorizeMember(userId_, pokerKey_, overrideMinStake_); 167 | } 168 | 169 | function slashReporter(uint256 slasherId_, uint256 times_) external override nonReentrant { 170 | require(clients[msg.sender].active, "INVALID_CLIENT"); 171 | require(clients[msg.sender].canSlash, "CANT_SLASH"); 172 | if (times_ == 0) { 173 | return; 174 | } 175 | 176 | POWER_POKE_STAKING.slashHDH(slasherId_, times_); 177 | } 178 | 179 | function reward( 180 | uint256 userId_, 181 | uint256 gasUsed_, 182 | uint256 compensationPlan_, 183 | bytes calldata pokeOptions_ 184 | ) external override nonReentrant whenNotPaused { 185 | RewardHelperStruct memory helper; 186 | require(clients[msg.sender].active, "INVALID_CLIENT"); 187 | 188 | PokeRewardOptions memory opts = abi.decode(pokeOptions_, (PokeRewardOptions)); 189 | if (opts.compensateInETH) { 190 | gasUsed_ = gasUsed_.add(clients[msg.sender].fixedCompensationETH); 191 | } else { 192 | gasUsed_ = gasUsed_.add(clients[msg.sender].fixedCompensationCVP); 193 | } 194 | 195 | if (gasUsed_ == 0) { 196 | return; 197 | } 198 | 199 | helper.ethPrice = IPowerOracleV3Reader(oracle).getPriceByAsset(WETH_TOKEN); 200 | helper.cvpPrice = IPowerOracleV3Reader(oracle).getPriceByAsset(address(CVP_TOKEN)); 201 | 202 | helper.gasPrice = getGasPriceFor(msg.sender); 203 | helper.compensationCVP = helper.gasPrice.mul(gasUsed_).mul(helper.ethPrice) / helper.cvpPrice; 204 | uint256 userDeposit = POWER_POKE_STAKING.getDepositOf(userId_); 205 | 206 | if (userDeposit != 0) { 207 | helper.bonusCVP = getPokerBonus(msg.sender, compensationPlan_, gasUsed_, userDeposit); 208 | } 209 | 210 | helper.totalInCVP = helper.compensationCVP.add(helper.bonusCVP); 211 | require(clients[msg.sender].credit >= helper.totalInCVP, "NOT_ENOUGH_CREDITS"); 212 | clients[msg.sender].credit = clients[msg.sender].credit.sub(helper.totalInCVP); 213 | 214 | if (opts.compensateInETH) { 215 | helper.earnedCVP = helper.bonusCVP; 216 | rewards[userId_] = rewards[userId_].add(helper.bonusCVP); 217 | helper.earnedETH = _payoutCompensationInETH(opts.to, helper.compensationCVP); 218 | } else { 219 | helper.earnedCVP = helper.compensationCVP.add(helper.bonusCVP); 220 | rewards[userId_] = rewards[userId_].add(helper.earnedCVP); 221 | } 222 | 223 | emit RewardUser( 224 | msg.sender, 225 | userId_, 226 | compensationPlan_, 227 | opts.compensateInETH, 228 | gasUsed_, 229 | helper.gasPrice, 230 | userDeposit, 231 | helper.ethPrice, 232 | helper.cvpPrice, 233 | helper.compensationCVP, 234 | helper.bonusCVP, 235 | helper.earnedCVP, 236 | helper.earnedETH 237 | ); 238 | } 239 | 240 | /*** CLIENT OWNER INTERFACE ***/ 241 | function transferClientOwnership(address client_, address to_) external override onlyClientOwner(client_) { 242 | clients[client_].owner = to_; 243 | emit TransferClientOwnership(client_, msg.sender, to_); 244 | } 245 | 246 | function addCredit(address client_, uint256 amount_) external override { 247 | Client storage client = clients[client_]; 248 | 249 | require(client.active, "ONLY_ACTIVE_CLIENT"); 250 | 251 | CVP_TOKEN.transferFrom(msg.sender, address(this), amount_); 252 | client.credit = client.credit.add(amount_); 253 | totalCredits = totalCredits.add(amount_); 254 | 255 | emit AddCredit(client_, amount_); 256 | } 257 | 258 | function withdrawCredit( 259 | address client_, 260 | address to_, 261 | uint256 amount_ 262 | ) external override onlyClientOwner(client_) { 263 | Client storage client = clients[client_]; 264 | 265 | client.credit = client.credit.sub(amount_); 266 | totalCredits = totalCredits.sub(amount_); 267 | 268 | CVP_TOKEN.transfer(to_, amount_); 269 | 270 | emit WithdrawCredit(client_, to_, amount_); 271 | } 272 | 273 | function setReportIntervals( 274 | address client_, 275 | uint256 minReportInterval_, 276 | uint256 maxReportInterval_ 277 | ) external override onlyClientOwner(client_) { 278 | require(maxReportInterval_ > minReportInterval_ && minReportInterval_ > 0, "INVALID_REPORT_INTERVALS"); 279 | clients[client_].minReportInterval = minReportInterval_; 280 | clients[client_].maxReportInterval = maxReportInterval_; 281 | emit SetReportIntervals(client_, minReportInterval_, maxReportInterval_); 282 | } 283 | 284 | function setSlasherHeartbeat(address client_, uint256 slasherHeartbeat_) external override onlyClientOwner(client_) { 285 | clients[client_].slasherHeartbeat = slasherHeartbeat_; 286 | emit SetSlasherHeartbeat(client_, slasherHeartbeat_); 287 | } 288 | 289 | function setGasPriceLimit(address client_, uint256 gasPriceLimit_) external override onlyClientOwner(client_) { 290 | clients[client_].gasPriceLimit = gasPriceLimit_; 291 | emit SetGasPriceLimit(client_, gasPriceLimit_); 292 | } 293 | 294 | function setFixedCompensations( 295 | address client_, 296 | uint256 eth_, 297 | uint256 cvp_ 298 | ) external override onlyClientOwner(client_) { 299 | clients[client_].fixedCompensationETH = eth_; 300 | clients[client_].fixedCompensationCVP = cvp_; 301 | emit SetFixedCompensations(client_, eth_, cvp_); 302 | } 303 | 304 | function setBonusPlan( 305 | address client_, 306 | uint256 planId_, 307 | bool active_, 308 | uint64 bonusNominator_, 309 | uint64 bonusDenominator_, 310 | uint64 perGas_ 311 | ) external override onlyClientOwner(client_) { 312 | bonusPlans[client_][planId_] = BonusPlan(active_, bonusNominator_, bonusDenominator_, perGas_); 313 | emit SetBonusPlan(client_, planId_, active_, bonusNominator_, bonusDenominator_, perGas_); 314 | } 315 | 316 | function setMinimalDeposit(address client_, uint256 defaultMinDeposit_) external override onlyClientOwner(client_) { 317 | clients[client_].defaultMinDeposit = defaultMinDeposit_; 318 | emit SetDefaultMinDeposit(client_, defaultMinDeposit_); 319 | } 320 | 321 | /*** POKER INTERFACE ***/ 322 | function withdrawRewards(uint256 userId_, address to_) external override { 323 | if (pokerKeyRewardWithdrawAllowance[userId_] == true) { 324 | POWER_POKE_STAKING.requireValidAdminOrPokerKey(userId_, msg.sender); 325 | } else { 326 | POWER_POKE_STAKING.requireValidAdminKey(userId_, msg.sender); 327 | } 328 | require(to_ != address(0), "0_ADDRESS"); 329 | uint256 rewardAmount = rewards[userId_]; 330 | require(rewardAmount > 0, "NOTHING_TO_WITHDRAW"); 331 | rewards[userId_] = 0; 332 | 333 | CVP_TOKEN.transfer(to_, rewardAmount); 334 | 335 | emit WithdrawRewards(userId_, to_, rewardAmount); 336 | } 337 | 338 | function setPokerKeyRewardWithdrawAllowance(uint256 userId_, bool allow_) external override { 339 | POWER_POKE_STAKING.requireValidAdminKey(userId_, msg.sender); 340 | pokerKeyRewardWithdrawAllowance[userId_] = allow_; 341 | emit SetPokerKeyRewardWithdrawAllowance(userId_, allow_); 342 | } 343 | 344 | /*** OWNER INTERFACE ***/ 345 | function addClient( 346 | address client_, 347 | address owner_, 348 | bool canSlash_, 349 | uint256 gasPriceLimit_, 350 | uint256 minReportInterval_, 351 | uint256 maxReportInterval_ 352 | ) external override onlyOwner { 353 | require(maxReportInterval_ > minReportInterval_ && minReportInterval_ > 0, "INVALID_REPORT_INTERVALS"); 354 | 355 | Client storage c = clients[client_]; 356 | c.active = true; 357 | c.canSlash = canSlash_; 358 | c.owner = owner_; 359 | c.gasPriceLimit = gasPriceLimit_; 360 | c.minReportInterval = minReportInterval_; 361 | c.maxReportInterval = maxReportInterval_; 362 | c.slasherHeartbeat = uint256(-1); 363 | 364 | emit AddClient(client_, owner_, canSlash_, gasPriceLimit_, minReportInterval_, maxReportInterval_, uint256(-1)); 365 | } 366 | 367 | function setClientActiveFlag(address client_, bool active_) external override onlyOwner { 368 | clients[client_].active = active_; 369 | emit SetClientActiveFlag(client_, active_); 370 | } 371 | 372 | function setCanSlashFlag(address client_, bool canSlash) external override onlyOwner { 373 | clients[client_].active = canSlash; 374 | emit SetCanSlashFlag(client_, canSlash); 375 | } 376 | 377 | function setOracle(address oracle_) external override onlyOwner { 378 | oracle = oracle_; 379 | emit SetOracle(oracle_); 380 | } 381 | 382 | /** 383 | * @notice The owner pauses reward-operation 384 | */ 385 | function pause() external override onlyOwner { 386 | _pause(); 387 | } 388 | 389 | /** 390 | * @notice The owner unpauses reward-operation 391 | */ 392 | function unpause() external override onlyOwner { 393 | _unpause(); 394 | } 395 | 396 | /*** INTERNAL HELPERS ***/ 397 | function _payoutCompensationInETH(address _to, uint256 _cvpAmount) internal returns (uint256) { 398 | CVP_TOKEN.approve(address(UNISWAP_ROUTER), _cvpAmount); 399 | 400 | address[] memory path = new address[](2); 401 | path[0] = address(CVP_TOKEN); 402 | path[1] = address(WETH_TOKEN); 403 | 404 | uint256[] memory amounts = UNISWAP_ROUTER.swapExactTokensForETH(_cvpAmount, uint256(0), path, _to, now.add(1800)); 405 | return amounts[1]; 406 | } 407 | 408 | function _latestFastGas() internal view returns (uint256) { 409 | return uint256(FAST_GAS_ORACLE.latestAnswer()); 410 | } 411 | 412 | /*** GETTERS ***/ 413 | function creditOf(address client_) external view override returns (uint256) { 414 | return clients[client_].credit; 415 | } 416 | 417 | function ownerOf(address client_) external view override returns (address) { 418 | return clients[client_].owner; 419 | } 420 | 421 | function getMinMaxReportIntervals(address client_) external view override returns (uint256 min, uint256 max) { 422 | return (clients[client_].minReportInterval, clients[client_].maxReportInterval); 423 | } 424 | 425 | function getSlasherHeartbeat(address client_) external view override returns (uint256) { 426 | return clients[client_].slasherHeartbeat; 427 | } 428 | 429 | function getGasPriceLimit(address client_) external view override returns (uint256) { 430 | return clients[client_].gasPriceLimit; 431 | } 432 | 433 | function getPokerBonus( 434 | address client_, 435 | uint256 bonusPlanId_, 436 | uint256 gasUsed_, 437 | uint256 userDeposit_ 438 | ) public view override returns (uint256) { 439 | BonusPlan memory plan = bonusPlans[client_][bonusPlanId_]; 440 | require(plan.active, "INACTIVE_BONUS_PLAN"); 441 | 442 | // gasUsed_ * userDeposit_ * plan.bonusNumerator / bonusDenominator / plan.perGas 443 | return gasUsed_.mul(userDeposit_).mul(plan.bonusNumerator) / plan.bonusDenominator / plan.perGas; 444 | } 445 | 446 | function getGasPriceFor(address client_) public view override returns (uint256) { 447 | return Math.min(tx.gasprice, Math.min(_latestFastGas(), clients[client_].gasPriceLimit)); 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /contracts/PowerPokeStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | import "@openzeppelin/upgrades-core/contracts/Initializable.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/math/SafeMath.sol"; 8 | import "./interfaces/IPowerPokeStaking.sol"; 9 | import "./utils/PowerOwnable.sol"; 10 | import "./utils/PowerPausable.sol"; 11 | import "./PowerPokeStakingStorageV1.sol"; 12 | 13 | contract PowerPokeStaking is IPowerPokeStaking, PowerOwnable, Initializable, PowerPausable, PowerPokeStakingStorageV1 { 14 | using SafeMath for uint256; 15 | 16 | uint256 public constant HUNDRED_PCT = 100 ether; 17 | 18 | /// @notice The event emitted when a new user is created 19 | event CreateUser(uint256 indexed userId, address indexed adminKey, address indexed pokerKey, uint256 initialDeposit); 20 | 21 | /// @notice The event emitted when an existing user is updated 22 | event UpdateUser(uint256 indexed userId, address indexed adminKey, address indexed pokerKey); 23 | 24 | /// @notice The event emitted when the user creates pending deposit 25 | event CreateDeposit( 26 | uint256 indexed userId, 27 | address indexed depositor, 28 | uint256 pendingTimeout, 29 | uint256 amount, 30 | uint256 pendingDepositAfter 31 | ); 32 | 33 | /// @notice The event emitted when the user transfers his deposit from pending to the active 34 | event ExecuteDeposit(uint256 indexed userId, uint256 pendingTimeout, uint256 amount, uint256 depositAfter); 35 | 36 | /// @notice The event emitted when the user creates pending deposit 37 | event CreateWithdrawal( 38 | uint256 indexed userId, 39 | uint256 pendingTimeout, 40 | uint256 amount, 41 | uint256 pendingWithdrawalAfter, 42 | uint256 depositAfter 43 | ); 44 | 45 | /// @notice The event emitted when a valid admin key withdraws funds from 46 | event ExecuteWithdrawal(uint256 indexed userId, address indexed to, uint256 pendingTimeout, uint256 amount); 47 | 48 | /// @notice The event emitted when the owner sets new slashing percent values, where 1ether == 1% 49 | event SetSlashingPct(uint256 slasherSlashingRewardPct, uint256 protocolSlashingRewardPct); 50 | 51 | /// @notice The event emitted when the owner sets new deposit and withdrawal timeouts 52 | event SetTimeouts(uint256 depositTimeout, uint256 withdrawalTimeout); 53 | 54 | /// @notice The event emitted when the owner sets a new PowerOracle linked contract 55 | event SetSlasher(address powerOracle); 56 | 57 | /// @notice The event emitted when an arbitrary user fixes an outdated reporter userId record 58 | event SetReporter(uint256 indexed reporterId, address indexed msgSender); 59 | 60 | /// @notice The event emitted when the PowerOracle contract requests to slash a user with the given ID 61 | event Slash(uint256 indexed slasherId, uint256 indexed reporterId, uint256 slasherReward, uint256 reservoirReward); 62 | 63 | /// @notice The event emitted when the existing reporter is replaced with a new one due some reason 64 | event ReporterChange( 65 | uint256 indexed prevId, 66 | uint256 indexed nextId, 67 | uint256 highestDepositPrev, 68 | uint256 actualDepositPrev, 69 | uint256 actualDepositNext 70 | ); 71 | 72 | /// @notice CVP token address 73 | IERC20 public immutable CVP_TOKEN; 74 | 75 | constructor(address cvpToken_) public { 76 | require(cvpToken_ != address(0), "CVP_ADDR_IS_0"); 77 | 78 | CVP_TOKEN = IERC20(cvpToken_); 79 | } 80 | 81 | function initialize( 82 | address owner_, 83 | address reservoir_, 84 | address slasher_, 85 | uint256 slasherSlashingRewardPct_, 86 | uint256 reservoirSlashingRewardPct_, 87 | uint256 depositTimeout_, 88 | uint256 withdrawTimeout_ 89 | ) external initializer { 90 | require(depositTimeout_ > 0, "DEPOSIT_TIMEOUT_IS_0"); 91 | require(withdrawTimeout_ > 0, "WITHDRAW_TIMEOUT_IS_0"); 92 | 93 | _transferOwnership(owner_); 94 | reservoir = reservoir_; 95 | slasher = slasher_; 96 | slasherSlashingRewardPct = slasherSlashingRewardPct_; 97 | protocolSlashingRewardPct = reservoirSlashingRewardPct_; 98 | depositTimeout = depositTimeout_; 99 | withdrawalTimeout = withdrawTimeout_; 100 | } 101 | 102 | /*** User Interface ***/ 103 | 104 | /** 105 | * @notice An arbitrary user deposits CVP stake to the contract for the given user ID 106 | * @param userId_ The user ID to make deposit for 107 | * @param amount_ The amount in CVP tokens to deposit 108 | */ 109 | function createDeposit(uint256 userId_, uint256 amount_) external override whenNotPaused { 110 | require(amount_ > 0, "MISSING_AMOUNT"); 111 | 112 | User storage user = users[userId_]; 113 | 114 | require(user.adminKey != address(0), "INVALID_USER"); 115 | 116 | _createDeposit(userId_, amount_); 117 | } 118 | 119 | function _createDeposit(uint256 userId_, uint256 amount_) internal { 120 | User storage user = users[userId_]; 121 | 122 | uint256 pendingDepositAfter = user.pendingDeposit.add(amount_); 123 | uint256 timeout = block.timestamp.add(depositTimeout); 124 | 125 | user.pendingDeposit = pendingDepositAfter; 126 | user.pendingDepositTimeout = timeout; 127 | 128 | emit CreateDeposit(userId_, msg.sender, timeout, amount_, pendingDepositAfter); 129 | CVP_TOKEN.transferFrom(msg.sender, address(this), amount_); 130 | } 131 | 132 | function executeDeposit(uint256 userId_) external override { 133 | User storage user = users[userId_]; 134 | uint256 amount = user.pendingDeposit; 135 | uint256 pendingDepositTimeout = user.pendingDepositTimeout; 136 | 137 | // check 138 | require(user.adminKey == msg.sender, "ONLY_ADMIN_ALLOWED"); 139 | require(amount > 0, "NO_PENDING_DEPOSIT"); 140 | require(block.timestamp >= pendingDepositTimeout, "TIMEOUT_NOT_PASSED"); 141 | 142 | // increment deposit 143 | uint256 depositAfter = user.deposit.add(amount); 144 | user.deposit = depositAfter; 145 | totalDeposit = totalDeposit.add(amount); 146 | 147 | // reset pending deposit 148 | user.pendingDeposit = 0; 149 | user.pendingDepositTimeout = 0; 150 | 151 | _lastDepositChange[userId_] = block.timestamp; 152 | 153 | _trySetHighestDepositHolder(userId_, depositAfter); 154 | 155 | emit ExecuteDeposit(userId_, pendingDepositTimeout, amount, depositAfter); 156 | } 157 | 158 | function _trySetHighestDepositHolder(uint256 candidateId_, uint256 candidateDepositAfter_) internal { 159 | uint256 prevHdhID = _hdhId; 160 | uint256 prevDeposit = users[prevHdhID].deposit; 161 | 162 | if (candidateDepositAfter_ > prevDeposit && prevHdhID != candidateId_) { 163 | emit ReporterChange(prevHdhID, candidateId_, _highestDeposit, users[prevHdhID].deposit, candidateDepositAfter_); 164 | 165 | _highestDeposit = candidateDepositAfter_; 166 | _hdhId = candidateId_; 167 | } 168 | } 169 | 170 | /** 171 | * @notice A valid users admin key withdraws the deposited stake form the contract 172 | * @param userId_ The user ID to withdraw deposit from 173 | * @param amount_ The amount in CVP tokens to withdraw 174 | */ 175 | function createWithdrawal(uint256 userId_, uint256 amount_) external override { 176 | require(amount_ > 0, "MISSING_AMOUNT"); 177 | 178 | User storage user = users[userId_]; 179 | require(msg.sender == user.adminKey, "ONLY_ADMIN_ALLOWED"); 180 | 181 | // decrement deposit 182 | uint256 depositBefore = user.deposit; 183 | require(amount_ <= depositBefore, "AMOUNT_EXCEEDS_DEPOSIT"); 184 | 185 | uint256 depositAfter = depositBefore - amount_; 186 | user.deposit = depositAfter; 187 | totalDeposit = totalDeposit.sub(amount_); 188 | 189 | // increment pending withdrawal 190 | uint256 pendingWithdrawalAfter = user.pendingWithdrawal.add(amount_); 191 | uint256 timeout = block.timestamp.add(withdrawalTimeout); 192 | user.pendingWithdrawal = pendingWithdrawalAfter; 193 | user.pendingWithdrawalTimeout = timeout; 194 | 195 | _lastDepositChange[userId_] = block.timestamp; 196 | 197 | emit CreateWithdrawal(userId_, timeout, amount_, pendingWithdrawalAfter, depositAfter); 198 | } 199 | 200 | function executeWithdrawal(uint256 userId_, address to_) external override { 201 | require(to_ != address(0), "CANT_WITHDRAW_TO_0"); 202 | 203 | User storage user = users[userId_]; 204 | 205 | uint256 pendingWithdrawalTimeout = user.pendingWithdrawalTimeout; 206 | uint256 amount = user.pendingWithdrawal; 207 | 208 | require(msg.sender == user.adminKey, "ONLY_ADMIN_ALLOWED"); 209 | require(amount > 0, "NO_PENDING_WITHDRAWAL"); 210 | require(block.timestamp >= pendingWithdrawalTimeout, "TIMEOUT_NOT_PASSED"); 211 | 212 | user.pendingWithdrawal = 0; 213 | user.pendingWithdrawalTimeout = 0; 214 | 215 | emit ExecuteWithdrawal(userId_, to_, pendingWithdrawalTimeout, amount); 216 | CVP_TOKEN.transfer(to_, amount); 217 | } 218 | 219 | /** 220 | * @notice Creates a new user ID and stores the given keys 221 | * @param adminKey_ The admin key for the new user 222 | * @param pokerKey_ The poker key for the new user 223 | * @param initialDeposit_ The initial deposit to be transferred to this contract 224 | */ 225 | function createUser( 226 | address adminKey_, 227 | address pokerKey_, 228 | uint256 initialDeposit_ 229 | ) external override whenNotPaused { 230 | uint256 userId = ++userIdCounter; 231 | 232 | users[userId] = User(adminKey_, pokerKey_, 0, 0, 0, 0, 0); 233 | 234 | emit CreateUser(userId, adminKey_, pokerKey_, initialDeposit_); 235 | 236 | if (initialDeposit_ > 0) { 237 | _createDeposit(userId, initialDeposit_); 238 | } 239 | } 240 | 241 | /** 242 | * @notice Updates an existing user, only the current adminKey is eligible calling this method. 243 | * @param adminKey_ The new admin key for the user 244 | * @param pokerKey_ The new poker key for the user 245 | */ 246 | function updateUser( 247 | uint256 userId_, 248 | address adminKey_, 249 | address pokerKey_ 250 | ) external override { 251 | User storage user = users[userId_]; 252 | require(msg.sender == user.adminKey, "ONLY_ADMIN_ALLOWED"); 253 | 254 | if (adminKey_ != user.adminKey) { 255 | user.adminKey = adminKey_; 256 | } 257 | if (pokerKey_ != user.pokerKey) { 258 | user.pokerKey = pokerKey_; 259 | } 260 | 261 | emit UpdateUser(userId_, adminKey_, pokerKey_); 262 | } 263 | 264 | /*** SLASHER INTERFACE ***/ 265 | 266 | /** 267 | * @notice Slashes the current reporter if it did not make poke() call during the given report interval 268 | * @param slasherId_ The slasher ID 269 | * @param times_ The multiplier for a single slashing percent 270 | */ 271 | function slashHDH(uint256 slasherId_, uint256 times_) external virtual override { 272 | require(msg.sender == slasher, "ONLY_SLASHER_ALLOWED"); 273 | 274 | uint256 hdhId = _hdhId; 275 | uint256 hdhDeposit = users[hdhId].deposit; 276 | 277 | (uint256 slasherReward, uint256 reservoirReward, ) = getSlashAmount(hdhId, times_); 278 | 279 | uint256 amount = slasherReward.add(reservoirReward); 280 | require(hdhDeposit >= amount, "INSUFFICIENT_HDH_DEPOSIT"); 281 | 282 | // users[reporterId].deposit = reporterDeposit - slasherReward - reservoirReward; 283 | users[hdhId].deposit = hdhDeposit.sub(amount); 284 | 285 | // totalDeposit = totalDeposit - reservoirReward; (slasherReward is kept on the contract) 286 | totalDeposit = totalDeposit.sub(reservoirReward); 287 | 288 | if (slasherReward > 0) { 289 | // uint256 slasherDepositAfter = users[slasherId_].deposit + slasherReward 290 | uint256 slasherDepositAfter = users[slasherId_].deposit.add(slasherReward); 291 | users[slasherId_].deposit = slasherDepositAfter; 292 | _trySetHighestDepositHolder(slasherId_, slasherDepositAfter); 293 | } 294 | 295 | if (reservoirReward > 0) { 296 | CVP_TOKEN.transfer(reservoir, reservoirReward); 297 | } 298 | 299 | emit Slash(slasherId_, hdhId, slasherReward, reservoirReward); 300 | } 301 | 302 | /*** OWNER INTERFACE ***/ 303 | 304 | /** 305 | * @notice The owner sets a new slasher address 306 | * @param slasher_ The slasher address to set 307 | */ 308 | function setSlasher(address slasher_) external override onlyOwner { 309 | slasher = slasher_; 310 | emit SetSlasher(slasher_); 311 | } 312 | 313 | /** 314 | * @notice The owner sets the new slashing percent values 315 | * @param slasherSlashingRewardPct_ The slasher share will be accrued on the slasher's deposit 316 | * @param protocolSlashingRewardPct_ The protocol share will immediately be transferred to reservoir 317 | */ 318 | function setSlashingPct(uint256 slasherSlashingRewardPct_, uint256 protocolSlashingRewardPct_) 319 | external 320 | override 321 | onlyOwner 322 | { 323 | require(slasherSlashingRewardPct_.add(protocolSlashingRewardPct_) <= HUNDRED_PCT, "INVALID_SUM"); 324 | 325 | slasherSlashingRewardPct = slasherSlashingRewardPct_; 326 | protocolSlashingRewardPct = protocolSlashingRewardPct_; 327 | emit SetSlashingPct(slasherSlashingRewardPct_, protocolSlashingRewardPct_); 328 | } 329 | 330 | function setTimeouts(uint256 depositTimeout_, uint256 withdrawalTimeout_) external override onlyOwner { 331 | depositTimeout = depositTimeout_; 332 | withdrawalTimeout = withdrawalTimeout_; 333 | emit SetTimeouts(depositTimeout_, withdrawalTimeout_); 334 | } 335 | 336 | /** 337 | * @notice The owner pauses poke*-operations 338 | */ 339 | function pause() external override onlyOwner { 340 | _pause(); 341 | } 342 | 343 | /** 344 | * @notice The owner unpauses poke*-operations 345 | */ 346 | function unpause() external override onlyOwner { 347 | _unpause(); 348 | } 349 | 350 | /*** PERMISSIONLESS INTERFACE ***/ 351 | 352 | /** 353 | * @notice Set a given address as a reporter if his deposit is higher than the current highestDeposit 354 | * @param candidateId_ Te candidate address to try 355 | */ 356 | function setHDH(uint256 candidateId_) external override { 357 | uint256 candidateDeposit = users[candidateId_].deposit; 358 | uint256 prevHdhId = _hdhId; 359 | uint256 currentReporterDeposit = users[prevHdhId].deposit; 360 | 361 | require(candidateDeposit > currentReporterDeposit, "INSUFFICIENT_CANDIDATE_DEPOSIT"); 362 | 363 | emit ReporterChange(prevHdhId, candidateId_, _highestDeposit, currentReporterDeposit, candidateDeposit); 364 | emit SetReporter(candidateId_, msg.sender); 365 | 366 | _highestDeposit = candidateDeposit; 367 | _hdhId = candidateId_; 368 | } 369 | 370 | /*** VIEWERS ***/ 371 | 372 | function getHDHID() external view override returns (uint256) { 373 | return _hdhId; 374 | } 375 | 376 | function getHighestDeposit() external view override returns (uint256) { 377 | return _highestDeposit; 378 | } 379 | 380 | function getDepositOf(uint256 userId_) external view override returns (uint256) { 381 | return users[userId_].deposit; 382 | } 383 | 384 | function getPendingDepositOf(uint256 userId_) external view override returns (uint256 balance, uint256 timeout) { 385 | return (users[userId_].pendingDeposit, users[userId_].pendingDepositTimeout); 386 | } 387 | 388 | function getPendingWithdrawalOf(uint256 userId_) external view override returns (uint256 balance, uint256 timeout) { 389 | return (users[userId_].pendingWithdrawal, users[userId_].pendingWithdrawalTimeout); 390 | } 391 | 392 | function getSlashAmount(uint256 slasheeId_, uint256 times_) 393 | public 394 | view 395 | override 396 | returns ( 397 | uint256 slasherReward, 398 | uint256 reservoirReward, 399 | uint256 totalSlash 400 | ) 401 | { 402 | uint256 product = times_.mul(users[slasheeId_].deposit); 403 | // slasherReward = times_ * reporterDeposit * slasherRewardPct / HUNDRED_PCT; 404 | slasherReward = product.mul(slasherSlashingRewardPct) / HUNDRED_PCT; 405 | // reservoirReward = times_ * reporterDeposit * reservoirSlashingRewardPct / HUNDRED_PCT; 406 | reservoirReward = product.mul(protocolSlashingRewardPct) / HUNDRED_PCT; 407 | // totalSlash = slasherReward + reservoirReward 408 | totalSlash = slasherReward.add(reservoirReward); 409 | } 410 | 411 | function getUserStatus( 412 | uint256 userId_, 413 | address pokerKey_, 414 | uint256 minDeposit_ 415 | ) external view override returns (UserStatus) { 416 | if (userId_ == _hdhId && users[userId_].pokerKey == pokerKey_) { 417 | return UserStatus.HDH; 418 | } 419 | if (users[userId_].deposit >= minDeposit_ && users[userId_].pokerKey == pokerKey_) { 420 | return UserStatus.MEMBER; 421 | } 422 | return UserStatus.UNAUTHORIZED; 423 | } 424 | 425 | function authorizeHDH(uint256 userId_, address pokerKey_) external view override { 426 | require(userId_ == _hdhId, "NOT_HDH"); 427 | require(users[userId_].pokerKey == pokerKey_, "INVALID_POKER_KEY"); 428 | } 429 | 430 | function authorizeNonHDH( 431 | uint256 userId_, 432 | address pokerKey_, 433 | uint256 minDeposit_ 434 | ) external view override { 435 | require(userId_ != _hdhId, "IS_HDH"); 436 | authorizeMember(userId_, pokerKey_, minDeposit_); 437 | } 438 | 439 | function authorizeMember( 440 | uint256 userId_, 441 | address pokerKey_, 442 | uint256 minDeposit_ 443 | ) public view override { 444 | require(users[userId_].deposit >= minDeposit_, "INSUFFICIENT_DEPOSIT"); 445 | require(users[userId_].pokerKey == pokerKey_, "INVALID_POKER_KEY"); 446 | } 447 | 448 | function requireValidAdminKey(uint256 userId_, address adminKey_) external view override { 449 | require(users[userId_].adminKey == adminKey_, "INVALID_AMIN_KEY"); 450 | } 451 | 452 | function requireValidAdminOrPokerKey(uint256 userId_, address adminOrPokerKey_) external view override { 453 | require( 454 | users[userId_].adminKey == adminOrPokerKey_ || users[userId_].pokerKey == adminOrPokerKey_, 455 | "INVALID_AMIN_OR_POKER_KEY" 456 | ); 457 | } 458 | 459 | function getLastDepositChange(uint256 userId_) external view override returns (uint256) { 460 | return _lastDepositChange[userId_]; 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /contracts/PowerPokeStakingStorageV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | contract PowerPokeStakingStorageV1 { 6 | struct User { 7 | address adminKey; 8 | address pokerKey; 9 | uint256 deposit; 10 | uint256 pendingDeposit; 11 | uint256 pendingDepositTimeout; 12 | uint256 pendingWithdrawal; 13 | uint256 pendingWithdrawalTimeout; 14 | } 15 | 16 | /// @notice The deposit timeout in seconds 17 | uint256 public depositTimeout; 18 | 19 | /// @notice The withdrawal timeout in seconds 20 | uint256 public withdrawalTimeout; 21 | 22 | /// @notice The reservoir which holds CVP tokens 23 | address public reservoir; 24 | 25 | /// @notice The slasher address (PowerPoke) 26 | address public slasher; 27 | 28 | /// @notice The total amount of all deposits 29 | uint256 public totalDeposit; 30 | 31 | /// @notice The share of a slasher in slashed deposit per one outdated asset (1 eth == 1%) 32 | uint256 public slasherSlashingRewardPct; 33 | 34 | /// @notice The share of the protocol(reservoir) in slashed deposit per one outdated asset (1 eth == 1%) 35 | uint256 public protocolSlashingRewardPct; 36 | 37 | /// @notice The incremented user ID counter. Is updated only within createUser function call 38 | uint256 public userIdCounter; 39 | 40 | /// @dev The highest deposit. Usually of the current reporterId. Is safe to be outdated. 41 | uint256 internal _highestDeposit; 42 | 43 | /// @dev The current highest deposit holder ID. 44 | uint256 internal _hdhId; 45 | 46 | /// @notice User details by it's ID 47 | mapping(uint256 => User) public users; 48 | 49 | /// @dev Last deposit change timestamp by user ID 50 | mapping(uint256 => uint256) internal _lastDepositChange; 51 | } 52 | -------------------------------------------------------------------------------- /contracts/PowerPokeStorageV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | pragma experimental ABIEncoderV2; 6 | 7 | import "./interfaces/IPowerOracleV3.sol"; 8 | 9 | contract PowerPokeStorageV1 { 10 | struct Client { 11 | bool active; 12 | bool canSlash; 13 | bool _deprecated; 14 | address owner; 15 | uint256 credit; 16 | uint256 minReportInterval; 17 | uint256 maxReportInterval; 18 | uint256 slasherHeartbeat; 19 | uint256 gasPriceLimit; 20 | uint256 defaultMinDeposit; 21 | uint256 fixedCompensationCVP; 22 | uint256 fixedCompensationETH; 23 | } 24 | 25 | struct BonusPlan { 26 | bool active; 27 | uint64 bonusNumerator; 28 | uint64 bonusDenominator; 29 | uint64 perGas; 30 | } 31 | 32 | address public oracle; 33 | 34 | uint256 public totalCredits; 35 | 36 | mapping(uint256 => uint256) public rewards; 37 | 38 | mapping(uint256 => bool) public pokerKeyRewardWithdrawAllowance; 39 | 40 | mapping(address => Client) public clients; 41 | 42 | mapping(address => mapping(uint256 => BonusPlan)) public bonusPlans; 43 | } 44 | -------------------------------------------------------------------------------- /contracts/Uniswap/UniswapV2Library.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "../interfaces/IUniswapV2Pair.sol"; 6 | import "@openzeppelin/contracts/math/SafeMath.sol"; 7 | 8 | library UniswapV2Library { 9 | using SafeMath for uint256; 10 | 11 | // returns sorted token addresses, used to handle return values from pairs sorted in this order 12 | function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { 13 | require(tokenA != tokenB, "UniswapV2Library: IDENTICAL_ADDRESSES"); 14 | (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 15 | require(token0 != address(0), "UniswapV2Library: ZERO_ADDRESS"); 16 | } 17 | 18 | // calculates the CREATE2 address for a pair without making any external calls 19 | function pairFor( 20 | address factory, 21 | address tokenA, 22 | address tokenB 23 | ) internal pure returns (address pair) { 24 | (address token0, address token1) = sortTokens(tokenA, tokenB); 25 | pair = address( 26 | uint256( 27 | keccak256( 28 | abi.encodePacked( 29 | hex"ff", 30 | factory, 31 | keccak256(abi.encodePacked(token0, token1)), 32 | hex"e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303" // init code hash 33 | ) 34 | ) 35 | ) 36 | ); 37 | } 38 | 39 | // fetches and sorts the reserves for a pair 40 | function getReserves( 41 | address factory, 42 | address tokenA, 43 | address tokenB 44 | ) internal view returns (uint256 reserveA, uint256 reserveB) { 45 | (address token0, ) = sortTokens(tokenA, tokenB); 46 | (uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); 47 | (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); 48 | } 49 | 50 | // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset 51 | function quote( 52 | uint256 amountA, 53 | uint256 reserveA, 54 | uint256 reserveB 55 | ) internal pure returns (uint256 amountB) { 56 | require(amountA > 0, "UniswapV2Library: INSUFFICIENT_AMOUNT"); 57 | require(reserveA > 0 && reserveB > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); 58 | amountB = amountA.mul(reserveB) / reserveA; 59 | } 60 | 61 | // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset 62 | function getAmountOut( 63 | uint256 amountIn, 64 | uint256 reserveIn, 65 | uint256 reserveOut 66 | ) internal pure returns (uint256 amountOut) { 67 | require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT"); 68 | require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); 69 | uint256 amountInWithFee = amountIn.mul(997); 70 | uint256 numerator = amountInWithFee.mul(reserveOut); 71 | uint256 denominator = reserveIn.mul(1000).add(amountInWithFee); 72 | amountOut = numerator / denominator; 73 | } 74 | 75 | // given an output amount of an asset and pair reserves, returns a required input amount of the other asset 76 | function getAmountIn( 77 | uint256 amountOut, 78 | uint256 reserveIn, 79 | uint256 reserveOut 80 | ) internal pure returns (uint256 amountIn) { 81 | require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT"); 82 | require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); 83 | uint256 numerator = reserveIn.mul(amountOut).mul(1000); 84 | uint256 denominator = reserveOut.sub(amountOut).mul(997); 85 | amountIn = (numerator / denominator).add(1); 86 | } 87 | 88 | // performs chained getAmountOut calculations on any number of pairs 89 | function getAmountsOut( 90 | address factory, 91 | uint256 amountIn, 92 | address[] memory path 93 | ) internal view returns (uint256[] memory amounts) { 94 | require(path.length >= 2, "UniswapV2Library: INVALID_PATH"); 95 | amounts = new uint256[](path.length); 96 | amounts[0] = amountIn; 97 | for (uint256 i; i < path.length - 1; i++) { 98 | (uint256 reserveIn, uint256 reserveOut) = getReserves(factory, path[i], path[i + 1]); 99 | amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); 100 | } 101 | } 102 | 103 | // performs chained getAmountIn calculations on any number of pairs 104 | function getAmountsIn( 105 | address factory, 106 | uint256 amountOut, 107 | address[] memory path 108 | ) internal view returns (uint256[] memory amounts) { 109 | require(path.length >= 2, "UniswapV2Library: INVALID_PATH"); 110 | amounts = new uint256[](path.length); 111 | amounts[amounts.length - 1] = amountOut; 112 | for (uint256 i = path.length - 1; i > 0; i--) { 113 | (uint256 reserveIn, uint256 reserveOut) = getReserves(factory, path[i - 1], path[i]); 114 | amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /contracts/Uniswap/UniswapV2OracleLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | import "../interfaces/IUniswapV2Pair.sol"; 4 | 5 | pragma solidity ^0.6.10; 6 | 7 | // Based on code from https://github.com/Uniswap/uniswap-v2-periphery 8 | 9 | // a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) 10 | library FixedPoint { 11 | // range: [0, 2**112 - 1] 12 | // resolution: 1 / 2**112 13 | struct uq112x112 { 14 | uint224 _x; 15 | } 16 | 17 | // returns a uq112x112 which represents the ratio of the numerator to the denominator 18 | // equivalent to encode(numerator).div(denominator) 19 | function fraction(uint112 numerator, uint112 denominator) internal pure returns (uq112x112 memory) { 20 | require(denominator > 0, "DIV_BY_ZERO"); 21 | return uq112x112((uint224(numerator) << 112) / denominator); 22 | } 23 | 24 | // decode a uq112x112 into a uint with 18 decimals of precision 25 | function decode112with18(uq112x112 memory self) internal pure returns (uint) { 26 | // we only have 256 - 224 = 32 bits to spare, so scaling up by ~60 bits is dangerous 27 | // instead, get close to: 28 | // (x * 1e18) >> 112 29 | // without risk of overflowing, e.g.: 30 | // (x) / 2 ** (112 - lg(1e18)) 31 | return uint(self._x) / 5192296858534827; 32 | } 33 | } 34 | 35 | // library with helper methods for oracles that are concerned with computing average prices 36 | library UniswapV2OracleLibrary { 37 | using FixedPoint for *; 38 | 39 | // helper function that returns the current block timestamp within the range of uint32, i.e. [0, 2**32 - 1] 40 | function currentBlockTimestamp() internal view returns (uint32) { 41 | return uint32(block.timestamp % 2 ** 32); 42 | } 43 | 44 | // produces the cumulative price using counterfactuals to save gas and avoid a call to sync. 45 | function currentCumulativePrices( 46 | address pair 47 | ) internal view returns (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) { 48 | blockTimestamp = currentBlockTimestamp(); 49 | price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); 50 | price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); 51 | 52 | // if time has elapsed since the last update on the pair, mock the accumulated price values 53 | (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves(); 54 | if (blockTimestampLast != blockTimestamp) { 55 | // subtraction overflow is desired 56 | uint32 timeElapsed = blockTimestamp - blockTimestampLast; 57 | // addition overflow is desired 58 | // counterfactual 59 | price0Cumulative += uint(FixedPoint.fraction(reserve1, reserve0)._x) * timeElapsed; 60 | // counterfactual 61 | price1Cumulative += uint(FixedPoint.fraction(reserve0, reserve1)._x) * timeElapsed; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /contracts/UniswapTWAPProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "@openzeppelin/contracts/math/SafeMath.sol"; 7 | import "./Uniswap/UniswapV2OracleLibrary.sol"; 8 | import "./PowerOracleReader.sol"; 9 | 10 | abstract contract UniswapTWAPProvider is PowerOracleReader { 11 | using FixedPoint for *; 12 | using SafeMath for uint256; 13 | 14 | /// @notice A common scaling factor to maintain precision 15 | uint public constant expScale = 1e18; 16 | 17 | /// @notice The event emitted when anchor price is updated 18 | event AnchorPriceUpdated(string symbol, bytes32 indexed symbolHash, uint anchorPrice, uint oldTimestamp, uint newTimestamp); 19 | 20 | /// @notice The event emitted when the uniswap window changes 21 | event UniswapWindowUpdated(bytes32 indexed symbolHash, uint oldTimestamp, uint newTimestamp, uint oldPrice, uint newPrice); 22 | 23 | /// @notice The minimum amount of time in seconds required for the old uniswap price accumulator to be replaced 24 | uint public immutable ANCHOR_PERIOD; 25 | 26 | constructor(uint256 anchorPeriod_) public { 27 | ANCHOR_PERIOD = anchorPeriod_; 28 | } 29 | 30 | /** 31 | * @dev Fetches the current token/eth price accumulator from uniswap. 32 | */ 33 | function currentCumulativePrice(TokenConfigUpdate memory config) internal view returns (uint) { 34 | (uint cumulativePrice0, uint cumulativePrice1,) = UniswapV2OracleLibrary.currentCumulativePrices(config.uniswapMarket); 35 | if (config.isUniswapReversed) { 36 | return cumulativePrice1; 37 | } else { 38 | return cumulativePrice0; 39 | } 40 | } 41 | 42 | /** 43 | * @dev Fetches the current eth/usd price from uniswap, with 6 decimals of precision. 44 | * Conversion factor is 1e18 for eth/usdc market, since we decode uniswap price statically with 18 decimals. 45 | */ 46 | function fetchEthPrice() internal returns (uint) { 47 | address token = tokenBySymbolHash[ethHash]; 48 | return fetchAnchorPrice("ETH", getActiveTokenConfig(token), getTokenUpdateConfig(token), ethBaseUnit); 49 | } 50 | 51 | function fetchCvpPrice(uint256 ethPrice) internal returns (uint) { 52 | address token = tokenBySymbolHash[cvpHash]; 53 | return fetchAnchorPrice("CVP", getActiveTokenConfig(token), getTokenUpdateConfig(token), ethPrice); 54 | } 55 | 56 | /** 57 | * @dev Fetches the current token/usd price from uniswap, with 6 decimals of precision. 58 | * @param conversionFactor 1e18 if seeking the ETH price, and a 6 decimal ETH-USDC price in the case of other assets 59 | */ 60 | function fetchAnchorPrice(string memory symbol, TokenConfig memory config, TokenConfigUpdate memory updateConfig, uint conversionFactor) internal virtual returns (uint) { 61 | (uint nowCumulativePrice, uint oldCumulativePrice, uint oldTimestamp) = pokeWindowValues(config.symbolHash, updateConfig); 62 | 63 | // This should be impossible, but better safe than sorry 64 | require(block.timestamp > oldTimestamp, "TOO_EARLY"); 65 | uint timeElapsed = block.timestamp - oldTimestamp; 66 | 67 | // Calculate uniswap time-weighted average price 68 | // Underflow is a property of the accumulators: https://uniswap.org/audit.html#orgc9b3190 69 | FixedPoint.uq112x112 memory priceAverage = FixedPoint.uq112x112(uint224((nowCumulativePrice - oldCumulativePrice) / timeElapsed)); 70 | uint rawUniswapPriceMantissa = priceAverage.decode112with18(); 71 | uint unscaledPriceMantissa = rawUniswapPriceMantissa.mul(conversionFactor); 72 | uint anchorPrice; 73 | 74 | // Adjust rawUniswapPrice according to the units of the non-ETH asset 75 | // In the case of ETH, we would have to scale by 1e6 / USDC_UNITS, but since baseUnit2 is 1e6 (USDC), it cancels 76 | if (updateConfig.isUniswapReversed) { 77 | // unscaledPriceMantissa * ethBaseUnit / config.baseUnit / expScale, but we simplify bc ethBaseUnit == expScale 78 | anchorPrice = unscaledPriceMantissa / uint256(config.baseUnit); 79 | } else { 80 | anchorPrice = unscaledPriceMantissa.mul(config.baseUnit) / ethBaseUnit / expScale; 81 | } 82 | 83 | emit AnchorPriceUpdated(symbol, config.symbolHash, anchorPrice, oldTimestamp, block.timestamp); 84 | 85 | return anchorPrice; 86 | } 87 | 88 | /** 89 | * @dev Get time-weighted average prices for a token at the current timestamp. 90 | * Update new and old observations of lagging window if period elapsed. 91 | */ 92 | function pokeWindowValues(bytes32 symbolHash, TokenConfigUpdate memory updateConfig) internal returns (uint, uint, uint) { 93 | uint cumulativePrice = currentCumulativePrice(updateConfig); 94 | 95 | Observation memory newObservation = newObservations[symbolHash]; 96 | 97 | // Update new and old observations if elapsed time is greater than or equal to anchor period 98 | uint timeElapsed = block.timestamp - newObservation.timestamp; 99 | if (timeElapsed >= ANCHOR_PERIOD) { 100 | oldObservations[symbolHash].timestamp = newObservation.timestamp; 101 | oldObservations[symbolHash].acc = newObservation.acc; 102 | 103 | newObservations[symbolHash].timestamp = block.timestamp; 104 | newObservations[symbolHash].acc = cumulativePrice; 105 | emit UniswapWindowUpdated(symbolHash, newObservation.timestamp, block.timestamp, newObservation.acc, cumulativePrice); 106 | } 107 | return (cumulativePrice, oldObservations[symbolHash].acc, oldObservations[symbolHash].timestamp); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /contracts/interfaces/BPoolInterface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.6.12; 4 | 5 | interface BPoolInterface { 6 | function getBalance(address) external view returns (uint256); 7 | 8 | function totalSupply() external view returns (uint256); 9 | 10 | function getCurrentTokens() external view returns (address[] memory tokens); 11 | } 12 | -------------------------------------------------------------------------------- /contracts/interfaces/IEACAggregatorProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | interface IEACAggregatorProxy { 6 | function latestAnswer() external view returns (int256); 7 | } 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC20Detailed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.12; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | interface IERC20Detailed is IERC20 { 7 | function symbol() external view returns (string calldata); 8 | 9 | function decimals() external view returns (uint8); 10 | } 11 | -------------------------------------------------------------------------------- /contracts/interfaces/IPowerOracleV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | pragma experimental ABIEncoderV2; 5 | 6 | interface IPowerOracleV2 { 7 | enum ReportInterval { LESS_THAN_MIN, OK, GREATER_THAN_MAX } 8 | 9 | enum PriceSource { 10 | FIXED_ETH, /// implies the fixedPrice is a constant multiple of the ETH price (which varies) 11 | FIXED_USD, /// implies the fixedPrice is a constant multiple of the USD price (which is 1) 12 | REPORTER /// implies the price is set by the reporter 13 | } 14 | 15 | struct TokenConfig { 16 | address cToken; 17 | address underlying; 18 | bytes32 symbolHash; 19 | uint256 baseUnit; 20 | PriceSource priceSource; 21 | uint256 fixedPrice; 22 | address uniswapMarket; 23 | bool isUniswapReversed; 24 | } 25 | 26 | function pokeFromReporter( 27 | uint256 reporterId_, 28 | string[] memory symbols_, 29 | bytes calldata rewardOpts 30 | ) external; 31 | 32 | function pokeFromSlasher( 33 | uint256 slasherId_, 34 | string[] memory symbols_, 35 | bytes calldata rewardOpts 36 | ) external; 37 | 38 | function poke(string[] memory symbols_) external; 39 | 40 | function slasherHeartbeat(uint256 slasherId) external; 41 | 42 | /*** Owner Interface ***/ 43 | function setPowerPoke(address powerOracleStaking) external; 44 | 45 | function pause() external; 46 | 47 | function unpause() external; 48 | 49 | /*** Token Management ***/ 50 | function maxTokens() external view returns (uint256); 51 | 52 | function numTokens() external view returns (uint256); 53 | 54 | function getTokenConfig(uint256 i) external view returns (TokenConfig memory); 55 | } 56 | -------------------------------------------------------------------------------- /contracts/interfaces/IPowerOracleV2Reader.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | pragma experimental ABIEncoderV2; 5 | 6 | interface IPowerOracleV2Reader { 7 | function getPriceByAsset(address token) external view returns (uint256); 8 | 9 | function getPriceBySymbol(string calldata symbol) external view returns (uint256); 10 | 11 | function getPriceBySymbolHash(bytes32 symbolHash) external view returns (uint256); 12 | 13 | function assetPrices(address token) external view returns (uint256); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/interfaces/IPowerOracleV3.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | pragma experimental ABIEncoderV2; 5 | 6 | interface IPowerOracle { 7 | enum ReportInterval { LESS_THAN_MIN, OK, GREATER_THAN_MAX } 8 | 9 | function pokeFromReporter( 10 | uint256 reporterId_, 11 | string[] memory symbols_, 12 | bytes calldata rewardOpts 13 | ) external; 14 | 15 | function pokeFromSlasher( 16 | uint256 slasherId_, 17 | string[] memory symbols_, 18 | bytes calldata rewardOpts 19 | ) external; 20 | 21 | function poke(string[] memory symbols_) external; 22 | 23 | function slasherHeartbeat(uint256 slasherId) external; 24 | 25 | /*** Owner Interface ***/ 26 | function setPowerPoke(address powerOracleStaking) external; 27 | 28 | function pause() external; 29 | 30 | function unpause() external; 31 | } 32 | -------------------------------------------------------------------------------- /contracts/interfaces/IPowerOracleV3Reader.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | pragma experimental ABIEncoderV2; 5 | 6 | interface IPowerOracleV3Reader { 7 | function getPriceByAsset(address token) external view returns (uint256); 8 | 9 | function getPriceBySymbol(string calldata symbol) external view returns (uint256); 10 | 11 | function getPriceBySymbolHash(bytes32 symbolHash) external view returns (uint256); 12 | 13 | function getAssetPrices(address[] calldata token) external view returns (uint256[] memory); 14 | 15 | function getAssetPrices18(address[] calldata token) external view returns (uint256[] memory); 16 | 17 | function getPriceByAsset18(address token) external view returns (uint256); 18 | 19 | function getPriceBySymbol18(string calldata symbol) external view returns (uint256); 20 | 21 | function getPriceBySymbolHash18(bytes32 symbolHash) external view returns (uint256); 22 | 23 | function assetPrices(address token) external view returns (uint256); 24 | } 25 | -------------------------------------------------------------------------------- /contracts/interfaces/IPowerOracleV3TokenManagement.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | pragma experimental ABIEncoderV2; 5 | 6 | interface IPowerOracleV3TokenManagement { 7 | function getTokens() external view returns (address[] memory); 8 | 9 | function getTokenCount() external view returns (uint256); 10 | } 11 | -------------------------------------------------------------------------------- /contracts/interfaces/IPowerPoke.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | pragma experimental ABIEncoderV2; 5 | 6 | interface IPowerPoke { 7 | /*** CLIENT'S CONTRACT INTERFACE ***/ 8 | function authorizeReporter(uint256 userId_, address pokerKey_) external view; 9 | 10 | function authorizeNonReporter(uint256 userId_, address pokerKey_) external view; 11 | 12 | function authorizeNonReporterWithDeposit( 13 | uint256 userId_, 14 | address pokerKey_, 15 | uint256 overrideMinDeposit_ 16 | ) external view; 17 | 18 | function authorizePoker(uint256 userId_, address pokerKey_) external view; 19 | 20 | function authorizePokerWithDeposit( 21 | uint256 userId_, 22 | address pokerKey_, 23 | uint256 overrideMinStake_ 24 | ) external view; 25 | 26 | function slashReporter(uint256 slasherId_, uint256 times_) external; 27 | 28 | function reward( 29 | uint256 userId_, 30 | uint256 gasUsed_, 31 | uint256 compensationPlan_, 32 | bytes calldata pokeOptions_ 33 | ) external; 34 | 35 | /*** CLIENT OWNER INTERFACE ***/ 36 | function transferClientOwnership(address client_, address to_) external; 37 | 38 | function addCredit(address client_, uint256 amount_) external; 39 | 40 | function withdrawCredit( 41 | address client_, 42 | address to_, 43 | uint256 amount_ 44 | ) external; 45 | 46 | function setReportIntervals( 47 | address client_, 48 | uint256 minReportInterval_, 49 | uint256 maxReportInterval_ 50 | ) external; 51 | 52 | function setSlasherHeartbeat(address client_, uint256 slasherHeartbeat_) external; 53 | 54 | function setGasPriceLimit(address client_, uint256 gasPriceLimit_) external; 55 | 56 | function setFixedCompensations( 57 | address client_, 58 | uint256 eth_, 59 | uint256 cvp_ 60 | ) external; 61 | 62 | function setBonusPlan( 63 | address client_, 64 | uint256 planId_, 65 | bool active_, 66 | uint64 bonusNominator_, 67 | uint64 bonusDenominator_, 68 | uint64 perGas_ 69 | ) external; 70 | 71 | function setMinimalDeposit(address client_, uint256 defaultMinDeposit_) external; 72 | 73 | /*** POKER INTERFACE ***/ 74 | function withdrawRewards(uint256 userId_, address to_) external; 75 | 76 | function setPokerKeyRewardWithdrawAllowance(uint256 userId_, bool allow_) external; 77 | 78 | /*** OWNER INTERFACE ***/ 79 | function addClient( 80 | address client_, 81 | address owner_, 82 | bool canSlash_, 83 | uint256 gasPriceLimit_, 84 | uint256 minReportInterval_, 85 | uint256 maxReportInterval_ 86 | ) external; 87 | 88 | function setClientActiveFlag(address client_, bool active_) external; 89 | 90 | function setCanSlashFlag(address client_, bool canSlash) external; 91 | 92 | function setOracle(address oracle_) external; 93 | 94 | function pause() external; 95 | 96 | function unpause() external; 97 | 98 | /*** GETTERS ***/ 99 | function creditOf(address client_) external view returns (uint256); 100 | 101 | function ownerOf(address client_) external view returns (address); 102 | 103 | function getMinMaxReportIntervals(address client_) external view returns (uint256 min, uint256 max); 104 | 105 | function getSlasherHeartbeat(address client_) external view returns (uint256); 106 | 107 | function getGasPriceLimit(address client_) external view returns (uint256); 108 | 109 | function getPokerBonus( 110 | address client_, 111 | uint256 bonusPlanId_, 112 | uint256 gasUsed_, 113 | uint256 userDeposit_ 114 | ) external view returns (uint256); 115 | 116 | function getGasPriceFor(address client_) external view returns (uint256); 117 | } 118 | -------------------------------------------------------------------------------- /contracts/interfaces/IPowerPokeStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | interface IPowerPokeStaking { 6 | enum UserStatus { UNAUTHORIZED, HDH, MEMBER } 7 | 8 | /*** User Interface ***/ 9 | function createDeposit(uint256 userId_, uint256 amount_) external; 10 | 11 | function executeDeposit(uint256 userId_) external; 12 | 13 | function createWithdrawal(uint256 userId_, uint256 amount_) external; 14 | 15 | function executeWithdrawal(uint256 userId_, address to_) external; 16 | 17 | function createUser( 18 | address adminKey_, 19 | address reporterKey_, 20 | uint256 depositAmount 21 | ) external; 22 | 23 | function updateUser( 24 | uint256 userId, 25 | address adminKey_, 26 | address reporterKey_ 27 | ) external; 28 | 29 | /*** Owner Interface ***/ 30 | function setSlasher(address slasher) external; 31 | 32 | function setSlashingPct(uint256 slasherRewardPct, uint256 reservoirRewardPct) external; 33 | 34 | function setTimeouts(uint256 depositTimeout_, uint256 withdrawalTimeout_) external; 35 | 36 | function pause() external; 37 | 38 | function unpause() external; 39 | 40 | /*** PowerOracle Contract Interface ***/ 41 | function slashHDH(uint256 slasherId_, uint256 times_) external; 42 | 43 | /*** Permissionless Interface ***/ 44 | function setHDH(uint256 candidateId_) external; 45 | 46 | /*** Viewers ***/ 47 | function getHDHID() external view returns (uint256); 48 | 49 | function getHighestDeposit() external view returns (uint256); 50 | 51 | function getDepositOf(uint256 userId) external view returns (uint256); 52 | 53 | function getPendingDepositOf(uint256 userId_) external view returns (uint256 balance, uint256 timeout); 54 | 55 | function getPendingWithdrawalOf(uint256 userId_) external view returns (uint256 balance, uint256 timeout); 56 | 57 | function getSlashAmount(uint256 slasheeId_, uint256 times_) 58 | external 59 | view 60 | returns ( 61 | uint256 slasherReward, 62 | uint256 reservoirReward, 63 | uint256 totalSlash 64 | ); 65 | 66 | function getUserStatus( 67 | uint256 userId_, 68 | address reporterKey_, 69 | uint256 minDeposit_ 70 | ) external view returns (UserStatus); 71 | 72 | function authorizeHDH(uint256 userId_, address reporterKey_) external view; 73 | 74 | function authorizeNonHDH( 75 | uint256 userId_, 76 | address pokerKey_, 77 | uint256 minDeposit_ 78 | ) external view; 79 | 80 | function authorizeMember( 81 | uint256 userId_, 82 | address reporterKey_, 83 | uint256 minDeposit_ 84 | ) external view; 85 | 86 | function requireValidAdminKey(uint256 userId_, address adminKey_) external view; 87 | 88 | function requireValidAdminOrPokerKey(uint256 userId_, address adminOrPokerKey_) external view; 89 | 90 | function getLastDepositChange(uint256 userId_) external view returns (uint256); 91 | } 92 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | interface IUniswapV2Factory { 6 | event PairCreated(address indexed token0, address indexed token1, address pair, uint256); 7 | 8 | function feeTo() external view returns (address); 9 | 10 | function feeToSetter() external view returns (address); 11 | 12 | function migrator() external view returns (address); 13 | 14 | function getPair(address tokenA, address tokenB) external view returns (address pair); 15 | 16 | function allPairs(uint256) external view returns (address pair); 17 | 18 | function allPairsLength() external view returns (uint256); 19 | 20 | function createPair(address tokenA, address tokenB) external returns (address pair); 21 | 22 | function setFeeTo(address) external; 23 | 24 | function setFeeToSetter(address) external; 25 | 26 | function setMigrator(address) external; 27 | } 28 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Pair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | interface IUniswapV2Pair { 6 | event Approval(address indexed owner, address indexed spender, uint256 value); 7 | event Transfer(address indexed from, address indexed to, uint256 value); 8 | 9 | function name() external pure returns (string memory); 10 | 11 | function symbol() external pure returns (string memory); 12 | 13 | function decimals() external pure returns (uint8); 14 | 15 | function totalSupply() external view returns (uint256); 16 | 17 | function balanceOf(address owner) external view returns (uint256); 18 | 19 | function allowance(address owner, address spender) external view returns (uint256); 20 | 21 | function approve(address spender, uint256 value) external returns (bool); 22 | 23 | function transfer(address to, uint256 value) external returns (bool); 24 | 25 | function transferFrom( 26 | address from, 27 | address to, 28 | uint256 value 29 | ) external returns (bool); 30 | 31 | function DOMAIN_SEPARATOR() external view returns (bytes32); 32 | 33 | function PERMIT_TYPEHASH() external pure returns (bytes32); 34 | 35 | function nonces(address owner) external view returns (uint256); 36 | 37 | function permit( 38 | address owner, 39 | address spender, 40 | uint256 value, 41 | uint256 deadline, 42 | uint8 v, 43 | bytes32 r, 44 | bytes32 s 45 | ) external; 46 | 47 | event Mint(address indexed sender, uint256 amount0, uint256 amount1); 48 | event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); 49 | event Swap( 50 | address indexed sender, 51 | uint256 amount0In, 52 | uint256 amount1In, 53 | uint256 amount0Out, 54 | uint256 amount1Out, 55 | address indexed to 56 | ); 57 | event Sync(uint112 reserve0, uint112 reserve1); 58 | 59 | function MINIMUM_LIQUIDITY() external pure returns (uint256); 60 | 61 | function factory() external view returns (address); 62 | 63 | function token0() external view returns (address); 64 | 65 | function token1() external view returns (address); 66 | 67 | function getReserves() 68 | external 69 | view 70 | returns ( 71 | uint112 reserve0, 72 | uint112 reserve1, 73 | uint32 blockTimestampLast 74 | ); 75 | 76 | function price0CumulativeLast() external view returns (uint256); 77 | 78 | function price1CumulativeLast() external view returns (uint256); 79 | 80 | function kLast() external view returns (uint256); 81 | 82 | function mint(address to) external returns (uint256 liquidity); 83 | 84 | function burn(address to) external returns (uint256 amount0, uint256 amount1); 85 | 86 | function swap( 87 | uint256 amount0Out, 88 | uint256 amount1Out, 89 | address to, 90 | bytes calldata data 91 | ) external; 92 | 93 | function skim(address to) external; 94 | 95 | function sync() external; 96 | 97 | function initialize(address, address) external; 98 | } 99 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Router02.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | interface IUniswapV2Router02 { 6 | function swapExactTokensForETH( 7 | uint256 amountIn, 8 | uint256 amountOutMin, 9 | address[] calldata path, 10 | address to, 11 | uint256 deadline 12 | ) external returns (uint256[] memory amounts); 13 | } 14 | -------------------------------------------------------------------------------- /contracts/mocks/MockCToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.10; 4 | 5 | contract MockCToken { 6 | address public underlying; 7 | 8 | constructor(address underlying_) public { 9 | underlying = underlying_; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/mocks/MockCVP.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract MockCVP is ERC20 { 8 | constructor(uint256 amount_) public ERC20("CVP", "CVP") { 9 | _mint(msg.sender, amount_); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/mocks/MockFastGasOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | contract MockFastGasOracle { 6 | uint256 public latestAnswer; 7 | 8 | constructor(uint256 latestAnswer_) public { 9 | latestAnswer = latestAnswer_; 10 | } 11 | 12 | function setLatestAnswer(uint256 latestAnswer_) external { 13 | latestAnswer = latestAnswer_; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/mocks/MockOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "../PowerOracle.sol"; 7 | 8 | contract MockOracle is PowerOracle { 9 | constructor(address cvpToken_, uint256 anchorPeriod_) public PowerOracle(cvpToken_, anchorPeriod_) {} 10 | 11 | mapping(bytes32 => uint256) public mockedAnchorPrices; 12 | 13 | function mockSetPrice(bytes32 symbolHash_, uint128 value_) external { 14 | prices[symbolHash_] = Price(uint128(block.timestamp), value_); 15 | } 16 | 17 | event MockFetchMockedAnchorPrice(string symbol); 18 | 19 | function fetchAnchorPrice( 20 | string memory symbol, 21 | TokenConfig memory config, 22 | TokenConfigUpdate memory updateConfig, 23 | uint256 conversionFactor 24 | ) internal override returns (uint256) { 25 | bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); 26 | if (mockedAnchorPrices[symbolHash] > 0) { 27 | emit MockFetchMockedAnchorPrice(symbol); 28 | return mockedAnchorPrices[symbolHash]; 29 | } else { 30 | return super.fetchAnchorPrice(symbol, config, updateConfig, conversionFactor); 31 | } 32 | } 33 | 34 | function mockSetAnchorPrice(string memory symbol, uint256 value) external { 35 | mockedAnchorPrices[keccak256(abi.encodePacked(symbol))] = value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /contracts/mocks/MockPoke.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "../PowerPoke.sol"; 7 | 8 | contract MockPoke is PowerPoke { 9 | constructor( 10 | address cvpToken_, 11 | address wethToken_, 12 | address fastGasOracle_, 13 | address uniswapRouter_, 14 | address powerPokeStaking_ 15 | ) public PowerPoke(cvpToken_, wethToken_, fastGasOracle_, uniswapRouter_, powerPokeStaking_) {} 16 | 17 | function mockSetReward(uint256 userId_, uint256 amount_) external { 18 | rewards[userId_] = amount_; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/mocks/MockProxyCall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.12; 3 | pragma experimental ABIEncoderV2; 4 | 5 | contract MockProxyCall { 6 | function makeCall(address destination, bytes calldata payload) external { 7 | (bool ok, bytes memory data) = destination.call(payload); 8 | 9 | if (!ok) { 10 | assembly { 11 | let size := returndatasize() 12 | revert(add(data, 32), size) 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /contracts/mocks/MockStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | 5 | import "../PowerPokeStaking.sol"; 6 | 7 | contract MockStaking is PowerPokeStaking { 8 | constructor(address cvpToken_) public PowerPokeStaking(cvpToken_) {} 9 | 10 | function mockSetTotalDeposit(uint256 totalDeposit_) external { 11 | totalDeposit = totalDeposit_; 12 | } 13 | 14 | event MockSlash(uint256 slasherId, uint256 times); 15 | 16 | function slashHDH(uint256 slasherId_, uint256 times_) external override(PowerPokeStaking) { 17 | emit MockSlash(slasherId_, times_); 18 | } 19 | 20 | function mockSetReporter(uint256 userId_, uint256 highestDeposit_) external { 21 | _hdhId = userId_; 22 | _highestDeposit = highestDeposit_; 23 | } 24 | 25 | function mockSetUser( 26 | uint256 userId_, 27 | address adminKey_, 28 | address pokerKey_, 29 | uint256 deposit_ 30 | ) external { 31 | users[userId_].adminKey = adminKey_; 32 | users[userId_].pokerKey = pokerKey_; 33 | users[userId_].deposit = deposit_; 34 | } 35 | 36 | function mockSetUserAdmin(uint256 userId_, address adminKey_) external { 37 | users[userId_].adminKey = adminKey_; 38 | } 39 | 40 | function mockSetUserDeposit(uint256 userId_, uint256 deposit_) external { 41 | users[userId_].deposit = deposit_; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/mocks/MockUniswapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | contract MockUniswapRouter { 6 | address public immutable factory; 7 | address public immutable WETH; 8 | 9 | modifier ensure(uint256 deadline) { 10 | require(deadline >= block.timestamp, "UniswapV2Router: EXPIRED"); 11 | _; 12 | } 13 | 14 | constructor(address _factory, address _WETH) public { 15 | factory = _factory; 16 | WETH = _WETH; 17 | } 18 | 19 | function swapExactTokensForETH( 20 | uint256 amountIn, 21 | uint256, 22 | address[] calldata, 23 | address payable to, 24 | uint256 deadline 25 | ) external ensure(deadline) returns (uint256[] memory amounts) { 26 | uint256 amount = (amountIn * 3) / 1600; 27 | to.transfer((amountIn * 3) / 1600); 28 | amounts = new uint256[](2); 29 | amounts[1] = amount; 30 | return amounts; 31 | } 32 | 33 | receive() external payable {} 34 | } 35 | -------------------------------------------------------------------------------- /contracts/mocks/MockUniswapTokenPair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.12; 4 | 5 | contract MockUniswapTokenPair { 6 | uint112 public reserve0; 7 | uint112 public reserve1; 8 | uint32 public blockTimestampLast; 9 | 10 | uint256 public price0CumulativeLast; 11 | uint256 public price1CumulativeLast; 12 | 13 | constructor( 14 | uint112 reserve0_, 15 | uint112 reserve1_, 16 | uint32 blockTimestampLast_, 17 | uint256 price0CumulativeLast_, 18 | uint256 price1CumulativeLast_ 19 | ) public { 20 | reserve0 = reserve0_; 21 | reserve1 = reserve1_; 22 | blockTimestampLast = blockTimestampLast_; 23 | price0CumulativeLast = price0CumulativeLast_; 24 | price1CumulativeLast = price1CumulativeLast_; 25 | } 26 | 27 | function update( 28 | uint112 reserve0_, 29 | uint112 reserve1_, 30 | uint32 blockTimestampLast_, 31 | uint256 price0CumulativeLast_, 32 | uint256 price1CumulativeLast_ 33 | ) public { 34 | reserve0 = reserve0_; 35 | reserve1 = reserve1_; 36 | blockTimestampLast = blockTimestampLast_; 37 | price0CumulativeLast = price0CumulativeLast_; 38 | price1CumulativeLast = price1CumulativeLast_; 39 | } 40 | 41 | function getReserves() 42 | external 43 | view 44 | returns ( 45 | uint112, 46 | uint112, 47 | uint32 48 | ) 49 | { 50 | return (reserve0, reserve1, blockTimestampLast); 51 | } 52 | 53 | function getReservesFraction(bool reversedMarket) external view returns (uint224) { 54 | require(reserve0 > 0, "Reserve is equal to 0"); 55 | require(reserve1 > 0, "Reserve is equal to 0"); 56 | if (reversedMarket) { 57 | return (uint224(reserve0) << 112) / reserve1; 58 | } else { 59 | return (uint224(reserve1) << 112) / reserve0; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /contracts/mocks/StubOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "../PowerOracle.sol"; 7 | 8 | contract StubOracle is PowerOracle { 9 | constructor(address cvpToken_, uint256 anchorPeriod_) public PowerOracle(cvpToken_, anchorPeriod_) {} 10 | 11 | function stubSetPrice(bytes32 symbolHash_, uint128 value_) external { 12 | prices[symbolHash_] = Price(uint128(block.timestamp), value_); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/mocks/StubStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | 5 | import "../PowerPokeStaking.sol"; 6 | 7 | contract StubStaking is PowerPokeStaking { 8 | constructor(address cvpToken_) public PowerPokeStaking(cvpToken_) {} 9 | 10 | function stubSetTotalDeposit(uint256 totalDeposit_) external { 11 | totalDeposit = totalDeposit_; 12 | } 13 | 14 | function stubSetReporter(uint256 userId_, uint256 highestDeposit_) external { 15 | _hdhId = userId_; 16 | _highestDeposit = highestDeposit_; 17 | } 18 | 19 | function stubSetUser( 20 | uint256 userId_, 21 | address adminKey_, 22 | address pokerKey_, 23 | uint256 deposit_ 24 | ) external { 25 | users[userId_].adminKey = adminKey_; 26 | users[userId_].pokerKey = pokerKey_; 27 | users[userId_].deposit = deposit_; 28 | } 29 | 30 | function stubSetUserDeposit(uint256 userId_, uint256 deposit_) external { 31 | users[userId_].deposit = deposit_; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/utils/PowerOwnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // A modified version of https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.0/contracts/access/Ownable.sol 4 | // with no GSN Context support and _transferOwnership internal method 5 | 6 | pragma solidity ^0.6.0; 7 | 8 | /** 9 | * @dev Contract module which provides a basic access control mechanism, where 10 | * there is an account (an owner) that can be granted exclusive access to 11 | * specific functions. 12 | * 13 | * By default, the owner account will be the one that deploys the contract. This 14 | * can later be changed with {transferOwnership}. 15 | * 16 | * This module is used through inheritance. It will make available the modifier 17 | * `onlyOwner`, which can be applied to your functions to restrict their use to 18 | * the owner. 19 | */ 20 | contract PowerOwnable { 21 | address private _owner; 22 | 23 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 24 | 25 | /** 26 | * @dev Initializes the contract setting the deployer as the initial owner. 27 | */ 28 | constructor() internal { 29 | _owner = msg.sender; 30 | emit OwnershipTransferred(address(0), msg.sender); 31 | } 32 | 33 | /** 34 | * @dev Returns the address of the current owner. 35 | */ 36 | function owner() public view returns (address) { 37 | return _owner; 38 | } 39 | 40 | /** 41 | * @dev Throws if called by any account other than the owner. 42 | */ 43 | modifier onlyOwner() { 44 | require(_owner == msg.sender, "NOT_THE_OWNER"); 45 | _; 46 | } 47 | 48 | /** 49 | * @dev Leaves the contract without owner. It will not be possible to call 50 | * `onlyOwner` functions anymore. Can only be called by the current owner. 51 | * 52 | * NOTE: Renouncing ownership will leave the contract without an owner, 53 | * thereby removing any functionality that is only available to the owner. 54 | */ 55 | function renounceOwnership() public virtual onlyOwner { 56 | emit OwnershipTransferred(_owner, address(0)); 57 | _owner = address(0); 58 | } 59 | 60 | /** 61 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 62 | * Can only be called by the current owner. 63 | */ 64 | function transferOwnership(address newOwner) public virtual onlyOwner { 65 | require(newOwner != address(0), "NEW_OWNER_IS_NULL"); 66 | emit OwnershipTransferred(_owner, newOwner); 67 | _owner = newOwner; 68 | } 69 | 70 | /** 71 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 72 | */ 73 | function _transferOwnership(address newOwner) internal { 74 | require(newOwner != address(0), "NEW_OWNER_IS_NULL"); 75 | emit OwnershipTransferred(_owner, newOwner); 76 | _owner = newOwner; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /contracts/utils/PowerPausable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // A modified version of https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.0/contracts/utils/Pausable.sol 4 | // with no GSN Context support and no construct 5 | 6 | pragma solidity ^0.6.0; 7 | 8 | /** 9 | * @dev Contract module which allows children to implement an emergency stop 10 | * mechanism that can be triggered by an authorized account. 11 | * 12 | * This module is used through inheritance. It will make available the 13 | * modifiers `whenNotPaused` and `whenPaused`, which can be applied to 14 | * the functions of your contract. Note that they will not be pausable by 15 | * simply including this module, only once the modifiers are put in place. 16 | */ 17 | contract PowerPausable { 18 | /** 19 | * @dev Emitted when the pause is triggered by `account`. 20 | */ 21 | event Paused(address account); 22 | 23 | /** 24 | * @dev Emitted when the pause is lifted by `account`. 25 | */ 26 | event Unpaused(address account); 27 | 28 | bool private _paused; 29 | 30 | /** 31 | * @dev Returns true if the contract is paused, and false otherwise. 32 | */ 33 | function paused() public view returns (bool) { 34 | return _paused; 35 | } 36 | 37 | /** 38 | * @dev Modifier to make a function callable only when the contract is not paused. 39 | * 40 | * Requirements: 41 | * 42 | * - The contract must not be paused. 43 | */ 44 | modifier whenNotPaused() { 45 | require(!_paused, "PAUSED"); 46 | _; 47 | } 48 | 49 | /** 50 | * @dev Modifier to make a function callable only when the contract is paused. 51 | * 52 | * Requirements: 53 | * 54 | * - The contract must be paused. 55 | */ 56 | modifier whenPaused() { 57 | require(_paused, "NOT_PAUSED"); 58 | _; 59 | } 60 | 61 | /** 62 | * @dev Triggers stopped state. 63 | * 64 | * Requirements: 65 | * 66 | * - The contract must not be paused. 67 | */ 68 | function _pause() internal virtual whenNotPaused { 69 | _paused = true; 70 | emit Paused(msg.sender); 71 | } 72 | 73 | /** 74 | * @dev Returns to normal state. 75 | * 76 | * Requirements: 77 | * 78 | * - The contract must be paused. 79 | */ 80 | function _unpause() internal virtual whenPaused { 81 | _paused = false; 82 | emit Unpaused(msg.sender); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-truffle5'); 2 | require('@nomiclabs/hardhat-etherscan'); 3 | require('solidity-coverage'); 4 | require('hardhat-contract-sizer'); 5 | require('hardhat-gas-reporter'); 6 | require('hardhat-typechain'); 7 | 8 | require('./tasks/fetchPairValues') 9 | require('./tasks/deployTestnet') 10 | require('./tasks/deployMainnet') 11 | require('./tasks/deployInstantUniswapPrice') 12 | require('./tasks/redeployOracleImplementation') 13 | 14 | 15 | const fs = require('fs'); 16 | const homeDir = require('os').homedir(); 17 | const _ = require('lodash'); 18 | 19 | function getAccounts(network) { 20 | const fileName = homeDir + '/.ethereum/' + network; 21 | if(!fs.existsSync(fileName)) { 22 | return []; 23 | } 24 | return [_.trim('0x' + fs.readFileSync(fileName, {encoding: 'utf8'}))]; 25 | } 26 | 27 | const gasLimit = 12 * 10 ** 6; 28 | 29 | const config = { 30 | analytics: { 31 | enabled: false, 32 | }, 33 | contractSizer: { 34 | alphaSort: false, 35 | runOnCompile: true, 36 | }, 37 | // defaultNetwork: 'buidlerevm', 38 | gasReporter: { 39 | currency: 'USD', 40 | enabled: !!(process.env.REPORT_GAS) 41 | }, 42 | mocha: { 43 | timeout: 20000 44 | }, 45 | networks: { 46 | hardhat: { 47 | gas: gasLimit, 48 | blockGasLimit: gasLimit, 49 | allowUnlimitedContractSize: true 50 | }, 51 | mainnet: { 52 | url: 'https://mainnet-eth.compound.finance', 53 | gasPrice: 81 * 10 ** 9, 54 | gasMultiplier: 1.5, 55 | accounts: getAccounts('mainnet'), 56 | gas: gasLimit, 57 | blockGasLimit: gasLimit 58 | }, 59 | kovan: { 60 | url: 'https://kovan-eth.compound.finance', 61 | gasPrice: 10 ** 9, 62 | gasMultiplier: 1.5, 63 | accounts: getAccounts('kovan'), 64 | gas: gasLimit, 65 | blockGasLimit: gasLimit 66 | }, 67 | mainnetfork: { 68 | url: 'http://127.0.0.1:8545/', 69 | // accounts: getAccounts('mainnet'), 70 | gasPrice: 150 * 10 ** 9, 71 | gasMultiplier: 1.5, 72 | timeout: 2000000, 73 | gas: gasLimit, 74 | blockGasLimit: gasLimit, 75 | }, 76 | local: { 77 | url: 'http://127.0.0.1:8545', 78 | }, 79 | coverage: { 80 | url: 'http://127.0.0.1:8555', 81 | }, 82 | }, 83 | paths: { 84 | artifacts: './artifacts', 85 | cache: './cache', 86 | coverage: './coverage', 87 | coverageJson: './coverage.json', 88 | root: './', 89 | sources: './contracts', 90 | tests: './test', 91 | }, 92 | solidity: { 93 | settings: { 94 | optimizer: { 95 | enabled: !!process.env.ETHERSCAN_KEY || process.env.COMPILE_TARGET === 'release', 96 | runs: 200, 97 | } 98 | }, 99 | version: '0.6.12' 100 | }, 101 | typechain: { 102 | outDir: 'typechain', 103 | target: 'ethers-v5', 104 | runOnCompile: false 105 | }, 106 | etherscan: { 107 | apiKey: process.env.ETHERSCAN_KEY 108 | } 109 | }; 110 | 111 | module.exports = config; 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powerpool/power-oracle", 3 | "description": "Power Oracle is a decentralized cross-chain price oracle working on Ethereum Main Network and sidechains.", 4 | "version": "1.0.0", 5 | "author": { 6 | "name": "PowerPool", 7 | "url": "https://powerpool.finance" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/powerpool-finance/power-oracle-contracts/issues" 11 | }, 12 | "devDependencies": { 13 | "@ethersproject/abstract-signer": "^5.0.1", 14 | "@ethersproject/bignumber": "^5.0.3", 15 | "@nomiclabs/hardhat-ethers": "^2.0.1", 16 | "@nomiclabs/hardhat-etherscan": "^2.0.1", 17 | "@nomiclabs/hardhat-truffle5": "^2.0.0", 18 | "@nomiclabs/hardhat-web3": "^2.0.0", 19 | "@openzeppelin/contracts": "^3.2.0", 20 | "@openzeppelin/test-helpers": "^0.5.6", 21 | "@openzeppelin/truffle-upgrades": "^1.0.2", 22 | "@powerpool/hardhat-ganache": "^2.0.0", 23 | "@typechain/ethers-v5": "^6.0.0", 24 | "bignumber.js": "^9.0.1", 25 | "chai": "^4.2.0", 26 | "dotenv": "^8.2.0", 27 | "eslint": "^7.4.0", 28 | "eslint-config-prettier": "^8.3.0", 29 | "ethereum-waffle": "^3.1.0", 30 | "ethers": "^5.0.13", 31 | "fs-extra": "^10.0.0", 32 | "hardhat": "^2.0.3", 33 | "hardhat-contract-sizer": "^2.0.0", 34 | "hardhat-gas-reporter": "^1.0.1", 35 | "lodash": "^4.17.20", 36 | "mocha": "^8.0.1", 37 | "p-iteration": "^1.1.8", 38 | "prettier": "^2.0.5", 39 | "prettier-plugin-solidity": "^1.0.0-alpha.54", 40 | "shelljs": "^0.8.4", 41 | "shx": "^0.3.2", 42 | "solc": "0.6.12", 43 | "solhint": "^3.0.0", 44 | "solhint-plugin-prettier": "^0.0.5", 45 | "solidity-coverage": "^0.7.11", 46 | "typechain": "^4.0.0" 47 | }, 48 | "files": [ 49 | "/contracts" 50 | ], 51 | "homepage": "https://github.com/powerpool-finance/power-oracle-contracts#readme", 52 | "keywords": [ 53 | "blockchain", 54 | "ethereum", 55 | "smart-contracts", 56 | "solidity" 57 | ], 58 | "license": "GPL-3.0", 59 | "publishConfig": { 60 | "access": "public" 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "https://github.com/powerpool-finance/power-oracle-contracts" 65 | }, 66 | "scripts": { 67 | "build": "yarn run compile && yarn run typechain", 68 | "clean": "hardhat clean", 69 | "compile": "hardhat compile", 70 | "compile-release": "COMPILE_TARGET=true hardhat compile", 71 | "coverage": "hardhat coverage --solcoverjs ./.solcover.js --network coverage --temp artifacts --testfiles \"./test/**/*.js\"", 72 | "deploy:testnet:local": "hardhat deploy-testnet --network local", 73 | "deploy:testnet:kovan": "hardhat deploy-testnet --network kovan", 74 | "lint:sol": "solhint --config ./.solhint.json \"contracts/**/*.sol\"", 75 | "lint:js": "eslint --config .eslintrc.json --ignore-path ./.eslintignore --ext .js .", 76 | "node": "hardhat node", 77 | "prettier": "prettier --config .prettierrc --write \"**/*.{js,json,md,sol,ts}\"", 78 | "prettier:sol": "prettier --config .prettierrc --write \"contracts/**/*.sol\"", 79 | "prettier:list-different": "prettier --config .prettierrc --list-different \"**/*.{js,json,md,sol,ts}\"", 80 | "test": "hardhat test --no-compile", 81 | "test:local": "hardhat test --network local", 82 | "pairs": "builder pair-details --network mainnet", 83 | "report:size": "hardhat size-contracts", 84 | "report:gas": "./scripts/gasUsedReport.sh", 85 | "typechain": "hardhat typechain" 86 | }, 87 | "dependencies": { 88 | "hardhat-typechain": "^0.3.4" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /scripts/gasUsedReport.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit script as soon as a command fails. 4 | set -o errexit 5 | 6 | # Executes cleanup function at script exit. 7 | trap cleanup EXIT 8 | 9 | cleanup() { 10 | # Kill the ganache instance that we started (if we started one and if it's still running). 11 | if [ -n "$ganache_pid" ] && ps -p $ganache_pid > /dev/null; then 12 | kill -9 $ganache_pid 13 | fi 14 | } 15 | 16 | ganache_port=8545 17 | 18 | ganache_running() { 19 | nc -z localhost "$ganache_port" 20 | } 21 | 22 | relayer_running() { 23 | nc -z localhost "$relayer_port" 24 | } 25 | 26 | start_ganache() { 27 | yarn run node > /dev/null & 28 | 29 | ganache_pid=$! 30 | 31 | echo "Waiting for Buidler RVM to launch on port "$ganache_port"..." 32 | 33 | while ! ganache_running; do 34 | sleep 0.1 # wait for 1/10 of the second before check again 35 | done 36 | 37 | echo "Buidler EVM launched!" 38 | } 39 | 40 | if ganache_running; then 41 | echo "Using existing Buidler EVM instance" 42 | else 43 | echo "Starting our own Buidler EVM instance" 44 | start_ganache 45 | fi 46 | 47 | REPORT_GAS=true yarn run test:local "$@" 48 | -------------------------------------------------------------------------------- /tasks/deployInstantUniswapPrice.js: -------------------------------------------------------------------------------- 1 | /* global task */ 2 | 3 | require('@nomiclabs/hardhat-truffle5'); 4 | 5 | task('deploy-instant-uniswap-price', 'Deploy instant uniswap price') 6 | .setAction(async () => { 7 | const InstantUniswapPrice = artifacts.require('InstantUniswapPrice'); 8 | 9 | const { web3 } = InstantUniswapPrice; 10 | const [deployer] = await web3.eth.getAccounts(); 11 | const sendOptions = {from: deployer}; 12 | 13 | const instantPrice = await InstantUniswapPrice.new(sendOptions); 14 | 15 | console.log('currentEthPriceInUsdc', web3.utils.fromWei(await instantPrice.currentEthPriceInUsdc(), 'ether')); 16 | console.log('instantPrice.currentTokenEthPrice CVP', web3.utils.fromWei(await instantPrice.currentTokenEthPrice('0x38e4adb44ef08f22f5b5b76a8f0c2d0dcbe7dca1'), 'ether')); 17 | console.log('instantPrice.currentTokenUsdcPrice CVP', web3.utils.fromWei(await instantPrice.currentTokenUsdcPrice('0x38e4adb44ef08f22f5b5b76a8f0c2d0dcbe7dca1'), 'ether')); 18 | console.log('instantPrice.currentTokenEthPrice WBTC', web3.utils.fromWei(await instantPrice.currentTokenEthPrice('0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'), 'ether')); 19 | console.log('instantPrice.currentTokenUsdcPrice WBTC', web3.utils.fromWei(await instantPrice.currentTokenUsdcPrice('0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'), 'ether')); 20 | 21 | console.log('instantPrice.balancerPoolEthTokensSum PIPT', web3.utils.fromWei(await instantPrice.balancerPoolEthTokensSum('0x26607ac599266b21d13c7acf7942c7701a8b699c'), 'ether')); 22 | console.log('instantPrice.balancerPoolUsdTokensSum PIPT', web3.utils.fromWei(await instantPrice.balancerPoolUsdTokensSum('0x26607ac599266b21d13c7acf7942c7701a8b699c'), 'ether')); 23 | console.log('instantPrice.balancerPoolEthTokensSum YETI', web3.utils.fromWei(await instantPrice.balancerPoolEthTokensSum('0xb4bebd34f6daafd808f73de0d10235a92fbb6c3d'), 'ether')); 24 | console.log('instantPrice.balancerPoolUsdTokensSum YETI', web3.utils.fromWei(await instantPrice.balancerPoolUsdTokensSum('0xb4bebd34f6daafd808f73de0d10235a92fbb6c3d'), 'ether')); 25 | 26 | console.log('instantPrice.balancerPoolUsdTokensSum BTC-WETH', web3.utils.fromWei(await instantPrice.balancerPoolUsdTokensSum('0x1eff8af5d577060ba4ac8a29a13525bb0ee2a3d5'), 'ether')); 27 | console.log('instantPrice.balancerPoolEthTokensSum BTC-WETH', web3.utils.fromWei(await instantPrice.balancerPoolEthTokensSum('0x1eff8af5d577060ba4ac8a29a13525bb0ee2a3d5'), 'ether')); 28 | console.log('instantPrice.contractUsdTokensSum BTC-WETH', web3.utils.fromWei(await instantPrice.contractUsdTokensSum('0x1eff8af5d577060ba4ac8a29a13525bb0ee2a3d5', ['0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2']), 'ether')); 29 | console.log('instantPrice.contractEthTokensSum BTC-WETH', web3.utils.fromWei(await instantPrice.contractEthTokensSum('0x1eff8af5d577060ba4ac8a29a13525bb0ee2a3d5', ['0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2']), 'ether')); 30 | console.log('instantPrice.amountToEther BTC', web3.utils.fromWei(await instantPrice.amountToEther('302627813983', '8'), 'ether')); 31 | console.log('instantPrice.ethTokensSum BTC', web3.utils.fromWei(await instantPrice.ethTokensSum(['0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'], ['302627813983']), 'ether')); 32 | console.log('instantPrice.usdcTokensSum BTC', web3.utils.fromWei(await instantPrice.usdcTokensSum(['0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'], ['302627813983']), 'ether')); 33 | 34 | console.log('Done'); 35 | }); 36 | 37 | module.exports = {}; 38 | -------------------------------------------------------------------------------- /tasks/deployMainnet.js: -------------------------------------------------------------------------------- 1 | /* global task */ 2 | 3 | const _ = require('lodash'); 4 | 5 | require('@nomiclabs/hardhat-truffle5'); 6 | 7 | task('deploy-mainnet', 'Deploys mainnet contracts') 8 | .setAction(async (__, { ethers, network }) => { 9 | const { deployProxied, ether, fromWei, gwei, impersonateAccount, ethUsed } = require('../test/helpers'); 10 | const { constants, time } = require('@openzeppelin/test-helpers'); 11 | 12 | const PowerPokeStaking = artifacts.require('PowerPokeStaking'); 13 | const PowerOracle = artifacts.require('PowerOracle'); 14 | const PowerPoke = artifacts.require('PowerPoke'); 15 | 16 | PowerPokeStaking.numberFormat = 'String'; 17 | PowerOracle.numberFormat = 'String'; 18 | 19 | const { web3 } = PowerPokeStaking; 20 | const [deployer, testAcc] = await web3.eth.getAccounts(); 21 | 22 | const proxyAddress = '0x019e14DA4538ae1BF0BCd8608ab8595c6c6181FB'; 23 | const cvpAddress = '0x38e4adB44ef08F22F5B5b76A8f0c2d0dCbE7DcA1'; 24 | const wethAddress = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; 25 | const gasPriceOracle = '0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C'; 26 | const uniswapRouterAddress = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; 27 | const oldOracle = await PowerOracle.at(proxyAddress); 28 | const numTokens = await oldOracle.numTokens(); 29 | console.log('numTokens', numTokens); 30 | let configs = []; 31 | for(let i = 0; i < numTokens; i++) { 32 | configs[i] = _.pick(await oldOracle.getTokenConfig(i), ['cToken', 'underlying', 'symbolHash', 'baseUnit', 'priceSource', 'fixedPrice', 'uniswapMarket', 'isUniswapReversed']); 33 | } 34 | 35 | const ANCHOR_PERIOD = 1800; 36 | // In seconds 37 | const MIN_REPORT_INTERVAL = 2700; 38 | // In seconds 39 | const MAX_REPORT_INTERVAL = 3600; 40 | const STAKE_CHANGE_INTERVAL = MAX_REPORT_INTERVAL; 41 | // In order to act as a slasher, a user should keep their deposit >= MIN_SLASHING_DEPOSIT 42 | const MIN_SLASHING_DEPOSIT = ether(5000); 43 | // A slasher reward in pct to the reporter deposit. Is multiplied to the outdated token count. 44 | const SLASHER_REWARD_PCT = '0';//ether('0.015'); 45 | // The protocol reward in pct to the reporter deposit. Is multiplied to the outdated token count. 46 | const RESERVOIR_REWARD_PCT = '0';//ether('0.005'); 47 | const BONUS_NUMERATOR = '7610350076'; 48 | const BONUS_DENUMERATOR = '10000000000000000'; 49 | // const BONUS_HEARTBEAT_NUMERATOR = '0'; 50 | // const BONUS_HEARTBEAT_DENUMERATOR = '1800000000000000'; 51 | const PER_GAS = '10000'; 52 | const MAX_GAS_PRICE = gwei(500); 53 | 54 | const OWNER = '0xB258302C3f209491d604165549079680708581Cc'; 55 | const PROXY_OWNER = OWNER; 56 | const RESERVOIR = '0x8EbC56A13Ae7e3cE27B960b16AA57efed3F4e79E'; 57 | 58 | console.log('Deployer address is', deployer); 59 | 60 | console.log('ETH before', web3.utils.fromWei(await web3.eth.getBalance(deployer))); 61 | console.log('>>> Deploying PowerOracleStaking...'); 62 | const staking = await deployProxied( 63 | PowerPokeStaking, 64 | [cvpAddress], 65 | [deployer, RESERVOIR, constants.ZERO_ADDRESS, SLASHER_REWARD_PCT, RESERVOIR_REWARD_PCT, STAKE_CHANGE_INTERVAL, STAKE_CHANGE_INTERVAL], 66 | { 67 | proxyAdminOwner: PROXY_OWNER, 68 | implementation: '' 69 | } 70 | ); 71 | console.log('>>> PowerOracleStaking (proxy) deployed at', staking.address); 72 | console.log('>>> PowerOracleStaking implementation deployed at', staking.initialImplementation.address); 73 | 74 | const powerPoke = await deployProxied( 75 | PowerPoke, 76 | [cvpAddress, wethAddress, gasPriceOracle, uniswapRouterAddress, staking.address], 77 | [deployer, constants.ZERO_ADDRESS], 78 | { 79 | proxyAdminOwner: PROXY_OWNER, 80 | implementation: '' 81 | } 82 | ); 83 | console.log('>>> PowerPoke (proxy) deployed at', powerPoke.address); 84 | console.log('>>> PowerPoke implementation deployed at', powerPoke.initialImplementation.address); 85 | 86 | // await staking.setSlasher(powerPoke.address); 87 | 88 | console.log('>>> Deploying PowerOracle...'); 89 | const oracle = await deployProxied( 90 | PowerOracle, 91 | [cvpAddress, ANCHOR_PERIOD, configs], 92 | [OWNER, powerPoke.address], 93 | { 94 | proxyAdminOwner: PROXY_OWNER, 95 | implementation: '' 96 | } 97 | ); 98 | console.log('>>> PowerOracle (proxy) deployed at', oracle.address); 99 | console.log('>>> PowerOracle implementation deployed at', oracle.initialImplementation.address); 100 | 101 | console.log('>>> Setting powerOracle address in powerOracleStaking'); 102 | await powerPoke.setOracle(oracle.address); 103 | 104 | await powerPoke.addClient(oracle.address, deployer, false, MAX_GAS_PRICE, MIN_REPORT_INTERVAL, MAX_REPORT_INTERVAL); 105 | await powerPoke.setMinimalDeposit(oracle.address, MIN_SLASHING_DEPOSIT); 106 | await powerPoke.setBonusPlan(oracle.address, '1', true, BONUS_NUMERATOR, BONUS_DENUMERATOR, PER_GAS); 107 | // await powerPoke.setBonusPlan(oracle.address, '2', true, BONUS_HEARTBEAT_NUMERATOR, BONUS_HEARTBEAT_DENUMERATOR, PER_GAS); 108 | await powerPoke.setSlasherHeartbeat(oracle.address, MIN_REPORT_INTERVAL); 109 | await powerPoke.setFixedCompensations(oracle.address, 260000, 99000); 110 | 111 | console.log('>>> Transferring powerStaking address to the owner'); 112 | await staking.transferOwnership(OWNER); 113 | await powerPoke.transferOwnership(OWNER); 114 | await powerPoke.transferClientOwnership(oracle.address, OWNER); 115 | console.log('ETH after', web3.utils.fromWei(await web3.eth.getBalance(deployer))); 116 | 117 | console.log('Done'); 118 | 119 | if (network.name !== 'mainnetfork') { 120 | return; 121 | } 122 | const symbolsToPoke = ['ETH', 'YFI', 'COMP', 'CVP', 'SNX', 'wNXM', 'MKR', 'UNI', 'UMA', 'AAVE', 'DAI', 'SUSHI', 'CREAM', 'AKRO', 'KP3R', 'PICKLE', 'GRT', 'WHITE']; 123 | const MockCVP = artifacts.require('MockCVP'); 124 | const cvpToken = await MockCVP.at(cvpAddress); 125 | const fromOwner = {from: OWNER}; 126 | await impersonateAccount(ethers, OWNER); 127 | 128 | const deposit = ether(200000); 129 | const slasherDeposit = ether(199999.9); 130 | 131 | await web3.eth.sendTransaction({ 132 | from: deployer, 133 | to: OWNER, 134 | value: ether(10), 135 | }) 136 | 137 | await cvpToken.approve(powerPoke.address, ether(10000), fromOwner); 138 | await powerPoke.addCredit(oracle.address, ether(10000), fromOwner); 139 | 140 | await cvpToken.transfer(deployer, deposit, fromOwner); 141 | await cvpToken.approve(staking.address, deposit, {from: deployer}); 142 | await staking.createUser(deployer, deployer, deposit, {from: deployer}); 143 | 144 | await oracle.poke(symbolsToPoke, {from: deployer}); 145 | 146 | await time.increase(MAX_REPORT_INTERVAL); 147 | 148 | await staking.executeDeposit('1',{from: deployer}); 149 | let pokeCount = 0; 150 | 151 | await poke(deployer, 1); 152 | 153 | await time.increase(MIN_REPORT_INTERVAL); 154 | 155 | await poke(deployer, 1); 156 | 157 | await time.increase(MIN_REPORT_INTERVAL); 158 | 159 | await poke(deployer, 1); 160 | 161 | await time.increase(MIN_REPORT_INTERVAL); 162 | 163 | await poke(deployer, 1); 164 | 165 | await cvpToken.transfer(testAcc, slasherDeposit, fromOwner); 166 | await cvpToken.approve(staking.address, slasherDeposit, {from: testAcc}); 167 | await staking.createUser(testAcc, testAcc, slasherDeposit, {from: testAcc}); 168 | 169 | await time.increase(MAX_REPORT_INTERVAL); 170 | 171 | await staking.executeDeposit(2,{from: testAcc}); 172 | // 173 | // await poke(testAcc, 2, 'pokeFromSlasher'); 174 | // 175 | // await time.increase(MIN_REPORT_INTERVAL); 176 | 177 | const res = await oracle.slasherHeartbeat(2, {from: testAcc}); 178 | console.log('\n\nslasherHeartbeat reward', fromWei(await powerPoke.rewards(2))); 179 | console.log('slasherHeartbeat gasUsed', res.receipt.gasUsed); 180 | 181 | async function poke(from, pokerId, pokeFunc = 'pokeFromReporter') { 182 | let {testAddress, testOpts} = await generateTestWalletAndCompensateOpts(web3, ethers, pokeCount === 1); 183 | console.log('\n>>> Making the ' + pokeFunc); 184 | const pokeOptions = {from, gasPrice: gwei('100')}; 185 | // console.log('getGasPriceFor', fromWei(await powerPoke.contract.methods.getGasPriceFor(oracle.address).call(pokeOptions), 'gwei')); 186 | 187 | let res = await oracle[pokeFunc](pokerId, symbolsToPoke,testOpts, pokeOptions) 188 | 189 | pokeCount++; 190 | const ethUsedByPoke = await ethUsed(web3, res.receipt); 191 | console.log('gasUsed', res.receipt.gasUsed); 192 | console.log('ethUsed', ethUsedByPoke); 193 | console.log('ethCompensated', fromWei(await web3.eth.getBalance(testAddress))); 194 | 195 | console.log('powerPoke rewards', fromWei(await powerPoke.rewards(pokerId))); 196 | await powerPoke.withdrawRewards(pokerId, from, {from}); 197 | // console.log('cvpToken.balanceOf(from)', fromWei(await cvpToken.balanceOf(from))); 198 | 199 | console.log('cvp price', fromWei(await oracle.assetPrices(cvpAddress))); 200 | } 201 | }); 202 | 203 | async function generateTestWalletAndCompensateOpts(web3, ethers, compensateInETH = true) { 204 | const testWallet = ethers.Wallet.createRandom(); 205 | const powerPokeOpts = web3.eth.abi.encodeParameter( 206 | { 207 | PowerPokeRewardOpts: { 208 | to: 'address', 209 | compensateInETH: 'bool' 210 | }, 211 | }, 212 | { 213 | to: testWallet.address, 214 | compensateInETH 215 | }, 216 | ); 217 | return { 218 | testAddress: testWallet.address, 219 | testOpts: powerPokeOpts 220 | } 221 | } 222 | 223 | module.exports = {}; 224 | -------------------------------------------------------------------------------- /tasks/deployTestnet.js: -------------------------------------------------------------------------------- 1 | /* global task */ 2 | require('@nomiclabs/hardhat-truffle5'); 3 | 4 | const pIteration = require('p-iteration'); 5 | 6 | task('deploy-testnet', 'Deploys testnet contracts') 7 | .setAction(async () => { 8 | const { deployProxied, ether, gwei, keccak256 } = require('../test/helpers'); 9 | const { getTokenConfigs } = require('../test/localHelpers'); 10 | const { constants } = require('@openzeppelin/test-helpers'); 11 | 12 | // const REPORT_REWARD_IN_ETH = ether('0.05'); 13 | // const MAX_CVP_REWARD = ether(15); 14 | const ANCHOR_PERIOD = 30; 15 | const MIN_REPORT_INTERVAL = 60; 16 | const MAX_REPORT_INTERVAL = 90; 17 | const MIN_SLASHING_DEPOSIT = ether(40); 18 | const SLASHER_REWARD_PCT = ether(15); 19 | const RESERVOIR_REWARD_PCT = ether(5); 20 | const MockCVP = artifacts.require('MockCVP'); 21 | const OWNER = '0xe7F2f6bb028E2c01C2C34e01BFFe5f534E7f1901'; 22 | // The same as deployer 23 | // const RESERVOIR = '0x0A243E1867F682D6c6e7b446a43800977ff58024'; 24 | const RESERVOIR = '0xfE2AB24d7855093E3d90aa298a676FEDA9fab7a0'; 25 | const POOL_ADDRESS = '0x56038007b8de3CDbFd19da17909DBDc2bB5c0c45'; 26 | 27 | const PowerPoke = artifacts.require('PowerPoke'); 28 | const PowerPokeStaking = artifacts.require('PowerPokeStaking'); 29 | const PowerOracle = artifacts.require('PowerOracle'); 30 | const MockWETH = artifacts.require('MockWETH'); 31 | const MockFastGasOracle = artifacts.require('MockFastGasOracle'); 32 | 33 | const { web3 } = PowerPokeStaking; 34 | 35 | PowerPokeStaking.numberFormat = 'String'; 36 | PowerOracle.numberFormat = 'String'; 37 | 38 | const [deployer] = await web3.eth.getAccounts(); 39 | console.log('Deployer address is', deployer); 40 | 41 | console.log('>>> Deploying CVP token...'); 42 | const cvpToken = await MockCVP.new(ether(2e9)); 43 | console.log('>>> CVP Token deployed at', cvpToken.address); 44 | 45 | const mockWeth = await MockWETH.new(); 46 | const uniswapRouter = mockWeth; 47 | const mockFastOracle = await MockFastGasOracle.new(gwei(2)); 48 | 49 | console.log('>>> Deploying PowerPokeStaking...'); 50 | const staking = await deployProxied( 51 | PowerPokeStaking, 52 | [cvpToken.address], 53 | [deployer, RESERVOIR, constants.ZERO_ADDRESS, SLASHER_REWARD_PCT, RESERVOIR_REWARD_PCT, 60 * 5, 60 * 5], 54 | { proxyAdminOwner: OWNER } 55 | ); 56 | console.log('>>> PowerPokeStaking (proxy) deployed at', staking.address); 57 | console.log('>>> PowerPokeStaking implementation deployed at', staking.initialImplementation.address); 58 | 59 | console.log('>>> Deploying PowerPoke...'); 60 | const powerPoke = await deployProxied( 61 | PowerPoke, 62 | [cvpToken.address, mockWeth.address, mockFastOracle.address, uniswapRouter.address, staking.address], 63 | [deployer, constants.ZERO_ADDRESS], 64 | { proxyAdminOwner: OWNER } 65 | ); 66 | console.log('>>> PowerPoke (proxy) deployed at', powerPoke.address); 67 | console.log('>>> PowerPoke implementation deployed at', powerPoke.initialImplementation.address); 68 | 69 | await staking.setSlasher(powerPoke.address); 70 | 71 | console.log('>>> Deploying PowerOracle...'); 72 | let oracle; 73 | let tokensSymbols = []; 74 | if(POOL_ADDRESS) { 75 | const IUniswapV2Factory = artifacts.require('IUniswapV2Factory'); 76 | const IERC20 = artifacts.require('IERC20Detailed'); 77 | // const BPool = artifacts.require('BPoolInterface'); 78 | // const bpool = await BPool.at(POOL_ADDRESS); 79 | const uniswapFactory = await IUniswapV2Factory.at('0x4b2387242d2E1415A7Ce9ee584082d4B9d796061'); 80 | const wethAddress = '0xed0F538448Cc27B1deF57feAc43201C79e6bDCf7'; 81 | const usdcAddress = '0xdbb2b2550bd5f6091756ed9bb674388283d42bf4'; 82 | const tokensConfig = [ 83 | {cToken: wethAddress, underlying: wethAddress, symbolHash: keccak256('ETH'), baseUnit: ether(1), priceSource: 2, fixedPrice: 0, uniswapMarket: await uniswapFactory.getPair(usdcAddress, wethAddress), isUniswapReversed: true}, 84 | ]; 85 | tokensSymbols.push('ETH'); 86 | const poolTokens = [ 87 | '0xB771f325877b18977A44e2A26c9B202E3a2F4E80', 88 | '0x07e081Bcc6Cd8B7Cb68D4B9dB36B46Dd9663E8b4', 89 | '0x8D27d7cb7569467FA1d2f860c90e5C0D79dC10FD', 90 | '0xD6bf1F32da9F194e3b5aaf40c056F5079317bE89', 91 | '0xB76C1c2C49ccB707De99DbE207dd8eAEDF9aA751', 92 | '0x73c82a86866699E6ecE9cE5b3113F4B63A93AF87', 93 | '0xb0b4f73240Ed3907e2550F35397D266E4E86A3a8', 94 | '0xED98CaEe836eA4dABbD5FDD9b2c34AB26a47FD41' 95 | ]; // = pool.getCurrentTokens(); 96 | 97 | await pIteration.forEach(poolTokens, async (tokenAddr) => { 98 | const token = await IERC20.at(tokenAddr); 99 | const symbol = await token.symbol(); 100 | tokensSymbols.push(symbol); 101 | tokensConfig.push({cToken: tokenAddr, underlying: tokenAddr, symbolHash: keccak256(symbol), baseUnit: ether(1), priceSource: 2, fixedPrice: 0, uniswapMarket: await uniswapFactory.getPair(tokenAddr, wethAddress), isUniswapReversed: false}) 102 | }) 103 | oracle = await deployProxied( 104 | PowerOracle, 105 | [cvpToken.address, ANCHOR_PERIOD, tokensConfig], 106 | [OWNER, powerPoke.address], 107 | { proxyAdminOwner: OWNER } 108 | ); 109 | } else { 110 | const tokenConfigs = await getTokenConfigs(cvpToken.address); 111 | tokensSymbols = ['ETH', 'DAI', 'REP', 'BTC', 'CVP']; 112 | oracle = await deployProxied( 113 | PowerOracle, 114 | [cvpToken.address, ANCHOR_PERIOD, tokenConfigs], 115 | [OWNER, powerPoke.address], 116 | { proxyAdminOwner: OWNER } 117 | ); 118 | } 119 | console.log('>>> PowerOracle (proxy) deployed at', oracle.address); 120 | console.log('>>> PowerOracle implementation deployed at', oracle.initialImplementation.address); 121 | 122 | console.log('>>> Setting powerOracle address in powerOracleStaking'); 123 | await powerPoke.setOracle(oracle.address); 124 | 125 | await powerPoke.addClient(oracle.address, OWNER, false, gwei(1.5), MIN_REPORT_INTERVAL, MAX_REPORT_INTERVAL); 126 | await powerPoke.setMinimalDeposit(oracle.address, MIN_SLASHING_DEPOSIT); 127 | 128 | console.log('>>> Transferring powerStaking address to the owner'); 129 | await staking.transferOwnership(OWNER); 130 | await powerPoke.transferOwnership(OWNER); 131 | await oracle.transferOwnership(OWNER); 132 | 133 | console.log('>>> Approving 10 000 CVP from fake reservoir (deployer) to PowerOracle'); 134 | await cvpToken.approve(oracle.address, 10000); 135 | 136 | console.log('>>> Making the initial poke'); 137 | console.log('tokensSymbols', tokensSymbols) 138 | await oracle.poke(tokensSymbols); 139 | 140 | console.log('Done'); 141 | }); 142 | 143 | module.exports = {}; 144 | -------------------------------------------------------------------------------- /tasks/fetchPairValues.js: -------------------------------------------------------------------------------- 1 | /* global task */ 2 | require('@nomiclabs/hardhat-truffle5'); 3 | 4 | task('pair-details', "Prints an account's balance") 5 | .addParam('account', "The account's address") 6 | .setAction(async (taskArgs) => { 7 | const MockUniswapTokenPair = artifacts.require('MockUniswapTokenPair'); 8 | MockUniswapTokenPair.numberFormat = 'String'; 9 | 10 | const pair = await MockUniswapTokenPair.at(taskArgs.account); 11 | 12 | // NOTICE: roughly values, but ok for seeding test suite 13 | console.log('price0CumulativeLast', await pair.price0CumulativeLast()); 14 | console.log('price1CumulativeLast', await pair.price1CumulativeLast()); 15 | }); 16 | 17 | module.exports = {}; 18 | -------------------------------------------------------------------------------- /tasks/redeployOracleImplementation.js: -------------------------------------------------------------------------------- 1 | /* global task */ 2 | 3 | require('@nomiclabs/hardhat-truffle5'); 4 | require('@nomiclabs/hardhat-ethers'); 5 | 6 | const pIteration = require('p-iteration'); 7 | const _ = require('lodash'); 8 | 9 | task('redeploy-oracle-implementation', 'Redeploy oracle implementation') 10 | .setAction(async (__, { ethers }) => { 11 | const { keccak256, forkContractUpgrade, deployAndSaveArgs, increaseTime } = require('../test/helpers'); 12 | const PowerOracle = artifacts.require('PowerOracle'); 13 | PowerOracle.numberFormat = 'String'; 14 | const { web3 } = PowerOracle; 15 | // const [deployer] = await web3.eth.getAccounts(); 16 | 17 | const proxyAddress = '0x019e14DA4538ae1BF0BCd8608ab8595c6c6181FB'; 18 | const oracle = await PowerOracle.at(proxyAddress); 19 | const numTokens = await oracle.numTokens(); 20 | console.log('numTokens', numTokens); 21 | let configs = []; 22 | for(let i = 0; i < numTokens; i++) { 23 | configs[i] = _.pick(await oracle.getTokenConfig(i), ['cToken', 'underlying', 'symbolHash', 'baseUnit', 'priceSource', 'fixedPrice', 'uniswapMarket', 'isUniswapReversed']); 24 | } 25 | 26 | configs = configs.filter(p => p.underlying.toLowerCase() !== '0x80fb784b7ed66730e8b1dbd9820afd29931aab03'); 27 | 28 | const addPairs = [ 29 | {market: '0x2e81ec0b8b4022fac83a21b2f2b4b8f5ed744d70', token: '0xc944e90c64b2c07662a292be6244bdf05cda44a7', symbol: keccak256('GRT'), isUniswapReversed: true} 30 | ]; 31 | await pIteration.forEachSeries(addPairs, (pair) => { 32 | configs.push({ 33 | cToken: pair.token, 34 | underlying: pair.token, 35 | symbolHash: pair.symbol, 36 | baseUnit: '1000000000000000000', 37 | priceSource: '2', 38 | fixedPrice: '0', 39 | uniswapMarket: pair.market, 40 | isUniswapReversed: !!pair.isUniswapReversed 41 | }) 42 | }); 43 | console.log('configs', configs.length); 44 | 45 | const cvpToken = await oracle.cvpToken(); 46 | const reservoir = await oracle.reservoir(); 47 | const anchorPeriod = await oracle.anchorPeriod(); 48 | const newImpl = await deployAndSaveArgs(PowerOracle, [cvpToken, reservoir, anchorPeriod, configs]); 49 | console.log('newImpl', newImpl.address); 50 | 51 | const networkId = await web3.eth.net.getId(); 52 | if (networkId === 1) { 53 | return; 54 | } 55 | 56 | await forkContractUpgrade( 57 | ethers, 58 | '0xb258302c3f209491d604165549079680708581cc', 59 | '0x7696f9208f9e195ba31e6f4B2D07B6462C8C42bb', 60 | '0x019e14DA4538ae1BF0BCd8608ab8595c6c6181FB', 61 | newImpl.address 62 | ); 63 | 64 | const symbols = ['GRT', 'YFI', 'COMP', 'CVP', 'SNX', 'wNXM', 'MKR', 'UNI', 'UMA', 'AAVE', 'DAI', 'SUSHI', 'CREAM', 'AKRO', 'COVER', 'KP3R', 'PICKLE']; 65 | 66 | await increaseTime(ethers, 60 * 60); 67 | 68 | await oracle.poke(symbols); 69 | 70 | await increaseTime(ethers, 60 * 60); 71 | 72 | await oracle.poke(symbols); 73 | 74 | await increaseTime(ethers, 60 * 60); 75 | 76 | await oracle.poke(symbols); 77 | 78 | await pIteration.forEachSeries(symbols, async (s) => { 79 | console.log(s, parseInt(await oracle.getPriceBySymbolHash(keccak256(s))) / 10 ** 6); 80 | }); 81 | 82 | console.log('Done'); 83 | }); 84 | 85 | module.exports = {}; 86 | -------------------------------------------------------------------------------- /test/Integration.test.js: -------------------------------------------------------------------------------- 1 | const { constants, time, expectEvent } = require('@openzeppelin/test-helpers'); 2 | const { ether, gwei, deployProxied, address, getEventArg } = require('./helpers'); 3 | const { getTokenConfigs } = require('./localHelpers'); 4 | const { solidity } = require('ethereum-waffle'); 5 | 6 | const chai = require('chai'); 7 | const MockCVP = artifacts.require('MockCVP'); 8 | const PowerPokeStaking = artifacts.require('PowerPokeStaking'); 9 | const PowerOracle = artifacts.require('PowerOracle'); 10 | const PowerPoke = artifacts.require('PowerPoke'); 11 | const MockFastGasOracle = artifacts.require('MockFastGasOracle'); 12 | 13 | chai.use(solidity); 14 | const { expect } = chai; 15 | 16 | MockCVP.numberFormat = 'String'; 17 | PowerPokeStaking.numberFormat = 'String'; 18 | PowerOracle.numberFormat = 'String'; 19 | 20 | const ANCHOR_PERIOD = 30; 21 | const MIN_REPORT_INTERVAL = 60; 22 | const MAX_REPORT_INTERVAL = 90; 23 | const SLASHER_REWARD_PCT = ether(15); 24 | const RESERVOIR_REWARD_PCT = ether(5); 25 | const GAS_PRICE_LIMIT = gwei(1000); 26 | const WETH = address(111); 27 | const DEPOSIT_TIMEOUT = '30'; 28 | const WITHDRAWAL_TIMEOUT = '180'; 29 | 30 | describe('IntegrationTest', function () { 31 | let staking; 32 | let oracle; 33 | let poke; 34 | let cvpToken; 35 | let powerPokeOpts; 36 | let fastGasOracle; 37 | 38 | let deployer, owner, reservoir, alice, bob, charlie, alicePoker, bobPoker, charlieReporter, sink, uniswapRouter, oracleClientOwner; 39 | 40 | before(async function() { 41 | [ 42 | deployer, 43 | owner, 44 | reservoir, 45 | alice, 46 | bob, 47 | charlie, 48 | alicePoker, 49 | bobPoker, 50 | charlieReporter, 51 | sink, 52 | uniswapRouter, 53 | oracleClientOwner, 54 | ] = await web3.eth.getAccounts(); 55 | fastGasOracle = await MockFastGasOracle.new(GAS_PRICE_LIMIT); 56 | powerPokeOpts = web3.eth.abi.encodeParameter( 57 | { 58 | PowerPokeRewardOpts: { 59 | to: 'address', 60 | compensateInETH: 'bool' 61 | }, 62 | }, 63 | { 64 | to: alice, 65 | compensateInETH: false 66 | }, 67 | ); 68 | }); 69 | 70 | beforeEach(async function() { 71 | cvpToken = await MockCVP.new(ether(2e9)); 72 | }); 73 | 74 | it('should allow stake, poke and slash', async function() { 75 | staking = await deployProxied( 76 | PowerPokeStaking, 77 | [cvpToken.address], 78 | [owner, reservoir, constants.ZERO_ADDRESS, SLASHER_REWARD_PCT, RESERVOIR_REWARD_PCT, DEPOSIT_TIMEOUT, WITHDRAWAL_TIMEOUT], 79 | { proxyAdminOwner: owner } 80 | ); 81 | 82 | poke = await deployProxied( 83 | PowerPoke, 84 | [cvpToken.address, WETH, fastGasOracle.address, uniswapRouter, staking.address], 85 | [owner, sink], 86 | { proxyAdminOwner: owner } 87 | ); 88 | 89 | oracle = await deployProxied( 90 | PowerOracle, 91 | [cvpToken.address, ANCHOR_PERIOD], 92 | [owner, poke.address], 93 | { proxyAdminOwner: owner } 94 | ); 95 | 96 | await oracle.addTokens(await getTokenConfigs(cvpToken.address), { from: owner }); 97 | await poke.setOracle(oracle.address, { from: owner }); 98 | await staking.setSlasher(poke.address, { from: owner }); 99 | 100 | await poke.addClient(oracle.address, oracleClientOwner, true, gwei(300), MIN_REPORT_INTERVAL, MAX_REPORT_INTERVAL, { from: owner }); 101 | await cvpToken.transfer(alice, ether(100000), { from: deployer }); 102 | await cvpToken.approve(poke.address, ether(30000), { from: alice }) 103 | await poke.addCredit(oracle.address, ether(30000), { from: alice }); 104 | await poke.setBonusPlan(oracle.address, 1, true, 25, 17520000, 100 * 1000, { from: oracleClientOwner }); 105 | 106 | expect(await staking.CVP_TOKEN()).to.be.equal(cvpToken.address); 107 | 108 | // Distribute funds... 109 | await cvpToken.transfer(reservoir, ether(100000), { from: deployer }); 110 | await cvpToken.transfer(alice, ether(1000), { from: deployer }); 111 | await cvpToken.transfer(bob, ether(1000), { from: deployer }); 112 | await cvpToken.transfer(charlie, ether(1000), { from: deployer }); 113 | 114 | // Approve funds... 115 | await cvpToken.approve(oracle.address, ether(100000), { from: reservoir }); 116 | await cvpToken.approve(staking.address, ether(100), { from: alice }); 117 | await cvpToken.approve(staking.address, ether(100), { from: bob }); 118 | await cvpToken.approve(staking.address, ether(100), { from: charlie }); 119 | 120 | // Register 121 | let res = await staking.createUser(alice, alicePoker, 0, { from: bob }); 122 | const aliceId = getEventArg(res, 'CreateUser', 'userId'); 123 | res = await staking.createUser(bob, bobPoker, 0, { from: alice }); 124 | const bobId = getEventArg(res, 'CreateUser', 'userId'); 125 | res = await staking.createUser(charlie, charlieReporter, 0, { from: charlie }); 126 | const charlieId = getEventArg(res, 'CreateUser', 'userId'); 127 | 128 | expect(aliceId).to.be.equal('1'); 129 | expect(bobId).to.be.equal('2'); 130 | expect(charlieId).to.be.equal('3'); 131 | 132 | // Create Deposits 133 | await staking.createDeposit(charlieId, ether(30), { from: charlie }); 134 | await staking.createDeposit(aliceId, ether(100), { from: alice }); 135 | await staking.createDeposit(bobId, ether(50), { from: bob }); 136 | 137 | expect(await staking.getDepositOf(aliceId)).to.be.equal(ether(0)); 138 | expect(await staking.getDepositOf(bobId)).to.be.equal(ether(0)); 139 | expect(await staking.getDepositOf(charlieId)).to.be.equal(ether(0)); 140 | let alicePendingDeposit = await staking.getPendingDepositOf(aliceId); 141 | let bobPendingDeposit = await staking.getPendingDepositOf(bobId); 142 | let charliePendingDeposit = await staking.getPendingDepositOf(charlieId); 143 | expect(alicePendingDeposit.balance).to.be.equal(ether(100)); 144 | expect(bobPendingDeposit.balance).to.be.equal(ether(50)); 145 | expect(charliePendingDeposit.balance).to.be.equal(ether(30)); 146 | 147 | await time.increase(DEPOSIT_TIMEOUT); 148 | 149 | // Execute Deposits 150 | await staking.executeDeposit(charlieId, { from: charlie }); 151 | await staking.executeDeposit(aliceId, { from: alice }); 152 | await staking.executeDeposit(bobId, { from: bob }); 153 | 154 | expect(await staking.getDepositOf(aliceId)).to.be.equal(ether(100)); 155 | expect(await staking.getDepositOf(bobId)).to.be.equal(ether(50)); 156 | expect(await staking.getDepositOf(charlieId)).to.be.equal(ether(30)); 157 | alicePendingDeposit = await staking.getPendingDepositOf(aliceId); 158 | bobPendingDeposit = await staking.getPendingDepositOf(bobId); 159 | charliePendingDeposit = await staking.getPendingDepositOf(charlieId); 160 | expect(alicePendingDeposit.balance).to.be.equal(ether(0)); 161 | expect(bobPendingDeposit.balance).to.be.equal(ether(0)); 162 | expect(charliePendingDeposit.balance).to.be.equal(ether(0)); 163 | 164 | expect(await staking.getHDHID()).to.be.equal(aliceId); 165 | expect(await staking.getHighestDeposit()).to.be.equal(ether(100)); 166 | 167 | // 1st Poke (Initial) 168 | res = await oracle.pokeFromReporter(aliceId, ['DAI', 'REP'], powerPokeOpts, { from: alicePoker }); 169 | 170 | expectEvent(res, 'PokeFromReporter', { 171 | reporterId: '1', 172 | tokenCount: '2', 173 | }) 174 | 175 | await expectEvent.inTransaction(res.tx, poke, 'RewardUser', { 176 | userId: '1', 177 | }) 178 | 179 | await time.increase(40); 180 | 181 | // 2nd Poke 182 | await expect(oracle.pokeFromReporter(aliceId, ['DAI', 'REP'], powerPokeOpts, { from: alicePoker })) 183 | .to.be.revertedWith('TOO_EARLY_UPDATE') 184 | 185 | await time.increase(65); 186 | 187 | // 3rd Poke 188 | res = await oracle.pokeFromReporter(aliceId, ['DAI', 'REP'], powerPokeOpts, { from: alicePoker }); 189 | 190 | expectEvent(res, 'PokeFromReporter', { 191 | reporterId: '1', 192 | tokenCount: '2', 193 | }) 194 | 195 | await expectEvent.inTransaction(res.tx, poke, 'RewardUser', { 196 | userId: '1', 197 | bonusPlan: '1', 198 | }) 199 | 200 | // 4th Poke from Slasher which fails 201 | await expect(oracle.pokeFromSlasher(bobId, ['DAI', 'REP'], powerPokeOpts, { from: bobPoker })) 202 | .to.be.revertedWith('INTERVAL_IS_OK'); 203 | 204 | await time.increase(95); 205 | 206 | // 5th Poke from Slasher which is successfull 207 | res = await oracle.pokeFromSlasher(bobId, ['DAI', 'REP'], powerPokeOpts, { from: bobPoker }); 208 | expectEvent(res, 'PokeFromSlasher', { 209 | slasherId: '2', 210 | tokenCount: '2', 211 | }) 212 | expect(await staking.getDepositOf(aliceId)).to.be.equal(ether(60)); 213 | 214 | // Withdrawing rewards 215 | await poke.withdrawRewards(aliceId, alice, { from: alice }); 216 | await expect(poke.withdrawRewards(aliceId, alice, { from: alice })) 217 | .to.be.revertedWith('NOTHING_TO_WITHDRAW'); 218 | 219 | // Withdraw stake 220 | await expect(staking.createWithdrawal(aliceId, ether(61), { from: alice })) 221 | .to.be.revertedWith('AMOUNT_EXCEEDS_DEPOSIT'); 222 | await staking.createWithdrawal(aliceId, ether(60), { from: alice }); 223 | await time.increase(WITHDRAWAL_TIMEOUT); 224 | await staking.executeWithdrawal(aliceId, alicePoker, { from: alice }); 225 | 226 | expect(await cvpToken.balanceOf(alicePoker)).to.be.equal(ether(60)); 227 | 228 | expect(await staking.getDepositOf(aliceId)).to.be.equal('0'); 229 | expect(await staking.getDepositOf(bobId)).to.be.equal(ether(80)); 230 | expect(await staking.getDepositOf(charlieId)).to.be.equal(ether(30)); 231 | 232 | expect(await staking.getHDHID()).to.be.equal(bobId); 233 | expect(await staking.getHighestDeposit()).to.be.equal(ether(80)); 234 | }); 235 | }); 236 | -------------------------------------------------------------------------------- /test/PowerOracleStaking.test.js: -------------------------------------------------------------------------------- 1 | const { constants, expectEvent, time } = require('@openzeppelin/test-helpers'); 2 | const { ether, strSum, deployProxied, getResTimestamp } = require('./helpers'); 3 | const { getTokenConfigs } = require('./localHelpers'); 4 | 5 | const chai = require('chai'); 6 | const MockCVP = artifacts.require('MockCVP'); 7 | const MockOracle = artifacts.require('MockOracle'); 8 | const StubStaking = artifacts.require('StubStaking'); 9 | 10 | const { expect } = chai; 11 | 12 | MockCVP.numberFormat = 'String'; 13 | StubStaking.numberFormat = 'String'; 14 | 15 | const MINIMAL_SLASHING_DEPOSIT = ether(50); 16 | const SLASHER_SLASHING_REWARD_PCT = ether(5); 17 | const PROTOCOL_SLASHING_REWARD_PCT = ether('1.5'); 18 | const DEPOSIT_TIMEOUT = '30'; 19 | const WITHDRAWAL_TIMEOUT = '180'; 20 | 21 | const USER_STATUS = { 22 | UNAUTHORIZED: '0', 23 | MEMBER: '1', 24 | HDH: '2' 25 | }; 26 | 27 | describe('PowerPokeStaking', function () { 28 | let staking; 29 | let cvpToken; 30 | 31 | let deployer, owner, powerOracle, powerPoke, alice, bob, charlie, alicePoker, bobPoker, charliePoker, sink, reservoir; 32 | 33 | before(async function() { 34 | [deployer, owner, powerOracle, powerPoke, alice, bob, charlie, alicePoker, bobPoker, charliePoker, sink, reservoir] = await web3.eth.getAccounts(); 35 | }); 36 | 37 | beforeEach(async function() { 38 | cvpToken = await MockCVP.new(ether(1e9)); 39 | staking = await deployProxied( 40 | StubStaking, 41 | [cvpToken.address], 42 | [owner, reservoir, powerOracle, SLASHER_SLASHING_REWARD_PCT, PROTOCOL_SLASHING_REWARD_PCT, DEPOSIT_TIMEOUT, WITHDRAWAL_TIMEOUT], 43 | { proxyAdminOwner: owner } 44 | ); 45 | }); 46 | 47 | describe('initialization', () => { 48 | it('should initialize correctly', async function() { 49 | expect(await staking.CVP_TOKEN()).to.be.equal(cvpToken.address); 50 | expect(await staking.owner()).to.be.equal(owner); 51 | expect(await staking.reservoir()).to.be.equal(reservoir); 52 | expect(await staking.slasher()).to.be.equal(powerOracle); 53 | expect(await staking.depositTimeout()).to.be.equal(DEPOSIT_TIMEOUT); 54 | expect(await staking.withdrawalTimeout()).to.be.equal(WITHDRAWAL_TIMEOUT); 55 | expect(await staking.slasherSlashingRewardPct()).to.be.equal(SLASHER_SLASHING_REWARD_PCT); 56 | expect(await staking.protocolSlashingRewardPct()).to.be.equal(PROTOCOL_SLASHING_REWARD_PCT); 57 | }); 58 | 59 | it('should deny initializing again', async function() { 60 | await expect(staking.initialize(owner, reservoir, powerOracle, SLASHER_SLASHING_REWARD_PCT, PROTOCOL_SLASHING_REWARD_PCT, DEPOSIT_TIMEOUT, WITHDRAWAL_TIMEOUT)) 61 | .to.be.revertedWith('Contract instance has already been initialized') 62 | }); 63 | }) 64 | 65 | describe('user interface', () => { 66 | describe('createUser', () => { 67 | it('should allow to create a user without initial deposit', async function() { 68 | const res = await staking.createUser(alice, alicePoker, 0, { from: bob }); 69 | expectEvent(res, 'CreateUser', { 70 | userId: '1', 71 | adminKey: alice, 72 | pokerKey: alicePoker, 73 | initialDeposit: '0' 74 | }) 75 | const user = await staking.users(1); 76 | expect(user.adminKey).to.equal(alice); 77 | expect(user.pokerKey).to.equal(alicePoker); 78 | expect(user.deposit).to.equal('0'); 79 | }); 80 | 81 | it('should allow to create a user with initial deposit', async function() { 82 | await cvpToken.transfer(bob, ether(1000), { from: deployer }); 83 | await cvpToken.approve(staking.address, ether(30), { from: bob }); 84 | 85 | const res = await staking.createUser(alice, alicePoker, ether(30), { from: bob }); 86 | const txAt = await getResTimestamp(res); 87 | expectEvent(res, 'CreateUser', { 88 | userId: '1', 89 | adminKey: alice, 90 | pokerKey: alicePoker, 91 | initialDeposit: ether(30) 92 | }) 93 | expectEvent(res, 'CreateDeposit', { 94 | userId: '1', 95 | depositor: bob, 96 | amount: ether(30), 97 | pendingDepositAfter: ether(30) 98 | }) 99 | 100 | const user = await staking.users(1); 101 | expect(user.adminKey).to.equal(alice); 102 | expect(user.pokerKey).to.equal(alicePoker); 103 | expect(user.deposit).to.equal('0'); 104 | expect(user.pendingDeposit).to.equal(ether(30)); 105 | 106 | const pendingDeposit = await staking.getPendingDepositOf(1); 107 | expect(pendingDeposit.balance).to.be.equal(ether(30)); 108 | expect(pendingDeposit.timeout).to.be.equal(strSum(txAt, DEPOSIT_TIMEOUT)); 109 | }); 110 | 111 | it('should correctly update id counter', async function() { 112 | let res = await staking.createUser(alice, alicePoker, 0, { from: bob }); 113 | expectEvent(res, 'CreateUser', { userId: '1' }); 114 | res = await staking.createUser(alice, alicePoker, 0, { from: bob }); 115 | expectEvent(res, 'CreateUser', { userId: '2' }); 116 | res = await staking.createUser(alice, alicePoker, 0, { from: bob }); 117 | expectEvent(res, 'CreateUser', { userId: '3' }); 118 | 119 | expect('0').to.be.eq(await staking.getLastDepositChange('1')); 120 | }); 121 | 122 | it('should deny creating a user when the contract is paused', async function() { 123 | await staking.pause({ from: owner }); 124 | await expect(staking.createUser(alice, alicePoker, 0, { from: bob })) 125 | .to.be.revertedWith('PAUSED'); 126 | }); 127 | }); 128 | 129 | describe('updateUser', () => { 130 | beforeEach(async () => { 131 | await staking.createUser(alice, alicePoker, 0, { from: bob }); 132 | }); 133 | 134 | it('should allow the current admin updating their keys', async function() { 135 | const res = await staking.updateUser(1, bob, bobPoker, { from: alice }); 136 | expectEvent(res, 'UpdateUser', { 137 | userId: '1', 138 | adminKey: bob, 139 | pokerKey: bobPoker, 140 | }); 141 | const user = await staking.users(1); 142 | expect(user.adminKey).to.equal(bob); 143 | expect(user.pokerKey).to.equal(bobPoker); 144 | }); 145 | 146 | it('should deny non-admin updating their keys', async function() { 147 | await expect(staking.updateUser(1, bob, bobPoker, { from: alicePoker })) 148 | .to.be.revertedWith('ONLY_ADMIN_ALLOWED'); 149 | }); 150 | }); 151 | 152 | describe('createDeposit', () => { 153 | beforeEach(async () => { 154 | await staking.createUser(alice, alicePoker, 0, { from: bob }); 155 | }); 156 | 157 | it('should allow anyone depositing multiple times for a given user ID', async function() { 158 | await cvpToken.transfer(bob, ether(50), { from: deployer }); 159 | await cvpToken.transfer(charlie, ether(50), { from: deployer }); 160 | await cvpToken.approve(staking.address, ether(50), { from: bob }); 161 | await cvpToken.approve(staking.address, ether(50), { from: charlie }); 162 | 163 | let res = await staking.createDeposit(1, ether(10), { from: bob }); 164 | let depositedAt = await getResTimestamp(res); 165 | expectEvent(res, 'CreateDeposit', { 166 | userId: '1', 167 | depositor: bob, 168 | pendingTimeout: strSum(depositedAt, DEPOSIT_TIMEOUT), 169 | amount: ether(10), 170 | pendingDepositAfter: ether(10) 171 | }) 172 | 173 | let user = await staking.users(1); 174 | expect(await user.pendingDeposit).to.be.equal(ether(10)); 175 | 176 | await time.increase(10); 177 | res = await staking.createDeposit(1, ether(20), { from: charlie }); 178 | depositedAt = await getResTimestamp(res); 179 | expectEvent(res, 'CreateDeposit', { 180 | userId: '1', 181 | depositor: charlie, 182 | pendingTimeout: strSum(depositedAt, DEPOSIT_TIMEOUT), 183 | amount: ether(20), 184 | pendingDepositAfter: ether(30) 185 | }) 186 | 187 | user = await staking.users(1); 188 | expect(await user.pendingDeposit).to.be.equal(ether(30)); 189 | }); 190 | 191 | it('should deny depositing 0 ', async function() { 192 | await expect(staking.createDeposit(1, 0, { from: bob })) 193 | .to.be.revertedWith('MISSING_AMOUNT'); 194 | }); 195 | 196 | it('should deny depositing for a non-existing user', async function() { 197 | await expect(staking.createDeposit(3, ether(30), { from: bob })) 198 | .to.be.revertedWith('INVALID_USER'); 199 | }); 200 | 201 | it('should deny creating a user when the contract is paused', async function() { 202 | await staking.pause({ from: owner }); 203 | await expect(staking.createDeposit(1, ether(10), { from: bob })) 204 | .to.be.revertedWith('PAUSED'); 205 | }); 206 | }) 207 | 208 | describe('executeDeposit', () => { 209 | let depositedAt; 210 | beforeEach(async () => { 211 | await staking.createUser(alice, alicePoker, 0, { from: bob }); 212 | await cvpToken.transfer(bob, ether(50), { from: deployer }); 213 | await cvpToken.transfer(charlie, ether(50), { from: deployer }); 214 | await cvpToken.approve(staking.address, ether(50), { from: bob }); 215 | await cvpToken.approve(staking.address, ether(50), { from: charlie }); 216 | }); 217 | 218 | it('should allow the adminKey executing deposit after the given timeout', async function() { 219 | let res = await staking.createDeposit(1, ether(10), { from: bob }); 220 | depositedAt = await getResTimestamp(res); 221 | await time.increase(DEPOSIT_TIMEOUT); 222 | res = await staking.executeDeposit(1, { from: alice }); 223 | expectEvent(res, 'ExecuteDeposit', { 224 | userId: '1', 225 | pendingTimeout: strSum(depositedAt, DEPOSIT_TIMEOUT), 226 | amount: ether(10), 227 | depositAfter: ether(10) 228 | }) 229 | let user = await staking.users(1); 230 | expect(user.deposit).to.equal(ether(10)); 231 | expect(user.pendingDeposit).to.equal(ether(0)); 232 | expect(await getResTimestamp(res)).to.be.eq(await staking.getLastDepositChange('1')); 233 | 234 | expect(await staking.totalDeposit()).to.be.equal(ether(10)); 235 | 236 | res = await staking.createDeposit(1, ether(20), { from: charlie }); 237 | depositedAt = await getResTimestamp(res); 238 | await time.increase(DEPOSIT_TIMEOUT); 239 | res = await staking.executeDeposit(1, { from: alice }); 240 | expectEvent(res, 'ExecuteDeposit', { 241 | userId: '1', 242 | pendingTimeout: strSum(depositedAt, DEPOSIT_TIMEOUT), 243 | amount: ether(20), 244 | depositAfter: ether(30) 245 | }) 246 | 247 | user = await staking.users(1); 248 | expect(user.deposit).to.equal(ether(30)); 249 | expect(user.pendingDeposit).to.equal(ether(0)); 250 | 251 | expect(await staking.totalDeposit()).to.be.equal(ether(30)); 252 | }); 253 | 254 | it('should update the reporter and the highest deposit values if needed', async function() { 255 | await staking.createUser(bob, bobPoker, 0, { from: bob }); 256 | 257 | await cvpToken.transfer(bob, ether(150), { from: deployer }); 258 | await cvpToken.approve(staking.address, ether(150), { from: bob }); 259 | 260 | expect(await staking.getHDHID()).to.be.equal('0'); 261 | 262 | await staking.createDeposit(1, ether(10), { from: bob }); 263 | await time.increase(DEPOSIT_TIMEOUT); 264 | let res = await staking.executeDeposit(1, { from: alice }); 265 | expectEvent(res, 'ReporterChange', { 266 | prevId: '0', 267 | nextId: '1', 268 | highestDepositPrev: '0', 269 | actualDepositPrev: '0', 270 | actualDepositNext: ether(10), 271 | }) 272 | 273 | await staking.createDeposit(2, ether(15), { from: bob }); 274 | await time.increase(DEPOSIT_TIMEOUT); 275 | res = await staking.executeDeposit(2, { from: bob }); 276 | expectEvent(res, 'ReporterChange', { 277 | prevId: '1', 278 | nextId: '2', 279 | highestDepositPrev: ether(10), 280 | actualDepositPrev: ether(10), 281 | actualDepositNext: ether(15), 282 | }) 283 | 284 | await staking.createDeposit(1, ether(5), { from: bob }); 285 | await time.increase(DEPOSIT_TIMEOUT); 286 | res = await staking.executeDeposit(1, { from: alice }); 287 | expectEvent.notEmitted(res, 'ReporterChange') 288 | }); 289 | 290 | it('should deny executing earlier than timeout', async function() { 291 | await staking.createDeposit(1, ether(5), { from: bob }); 292 | await time.increase(DEPOSIT_TIMEOUT - 10); 293 | await expect(staking.executeDeposit(1, { from: alice })) 294 | .to.be.revertedWith('TIMEOUT_NOT_PASSED'); 295 | }); 296 | 297 | it('should deny executing with 0 pending deposit', async function() { 298 | await expect(staking.executeDeposit(1, { from: alice })) 299 | .to.be.revertedWith('NO_PENDING_DEPOSIT'); 300 | }); 301 | 302 | it('should deny executing for a non-existing user', async function() { 303 | await expect(staking.executeDeposit(3, { from: bob })) 304 | .to.be.revertedWith('ONLY_ADMIN_ALLOWED'); 305 | }); 306 | }) 307 | 308 | describe('createWithdrawal', () => { 309 | const USER_ID = 42; 310 | 311 | beforeEach(async () => { 312 | await staking.stubSetUser(USER_ID, alice, alicePoker, ether(100), { from: bob }); 313 | await staking.stubSetTotalDeposit(ether(100)); 314 | await cvpToken.transfer(staking.address, ether(500), { from: deployer }); 315 | }); 316 | 317 | it('should allow the users admin key creating withdrawal all the deposit', async function() { 318 | expect(await cvpToken.balanceOf(sink)).to.be.equal('0'); 319 | expect(await staking.totalDeposit()).to.be.equal(ether('100')); 320 | 321 | const res = await staking.createWithdrawal(USER_ID, ether(100), { from: alice }); 322 | let createdAt = await getResTimestamp(res); 323 | 324 | expect(await getResTimestamp(res)).to.be.eq(await staking.getLastDepositChange(USER_ID)); 325 | 326 | expect(await staking.totalDeposit()).to.be.equal(ether('0')); 327 | 328 | expectEvent(res, 'CreateWithdrawal', { 329 | userId: '42', 330 | pendingTimeout: strSum(createdAt, WITHDRAWAL_TIMEOUT), 331 | amount: ether(100), 332 | pendingWithdrawalAfter: ether(100), 333 | depositAfter: ether(0) 334 | }); 335 | 336 | let user = await staking.users(USER_ID); 337 | expect(user.deposit).to.be.equal(ether(0)); 338 | expect(user.pendingWithdrawal).to.be.equal(ether(100)); 339 | }); 340 | 341 | it('should allow the users admin key withdrawing part of the deposit', async function() { 342 | expect(await cvpToken.balanceOf(sink)).to.be.equal('0'); 343 | expect(await staking.totalDeposit()).to.be.equal(ether('100')); 344 | 345 | const res = await staking.createWithdrawal(USER_ID, ether(80), { from: alice }); 346 | let createdAt = await getResTimestamp(res); 347 | 348 | expect(await staking.totalDeposit()).to.be.equal(ether('20')); 349 | 350 | expectEvent(res, 'CreateWithdrawal', { 351 | userId: '42', 352 | pendingTimeout: strSum(createdAt, WITHDRAWAL_TIMEOUT), 353 | amount: ether(80), 354 | pendingWithdrawalAfter: ether(80), 355 | depositAfter: ether(20) 356 | }); 357 | 358 | let user = await staking.users(USER_ID); 359 | expect(user.deposit).to.be.equal(ether(20)); 360 | expect(user.pendingWithdrawal).to.be.equal(ether(80)); 361 | }); 362 | 363 | it('should deny non-admin withdrawing rewards', async function() { 364 | await expect(staking.createWithdrawal(USER_ID, ether(30), { from: alicePoker })) 365 | .to.be.revertedWith('ONLY_ADMIN_ALLOWED'); 366 | }); 367 | 368 | it('should deny withdrawing more than the rewards balance', async function() { 369 | await expect(staking.createWithdrawal(USER_ID, ether(101), { from: alice })) 370 | .to.be.revertedWith('AMOUNT_EXCEEDS_DEPOSIT'); 371 | }); 372 | 373 | it('should deny withdrawing 0 balance', async function() { 374 | await expect(staking.createWithdrawal(USER_ID, 0, { from: alice })) 375 | .to.be.revertedWith('MISSING_AMOUNT'); 376 | }); 377 | }); 378 | 379 | describe('executeWithdrawal', () => { 380 | const USER_ID = 42; 381 | let createdAt; 382 | 383 | beforeEach(async () => { 384 | await staking.stubSetUser(USER_ID, alice, alicePoker, ether(100), { from: bob }); 385 | await staking.stubSetTotalDeposit(ether(100)); 386 | await cvpToken.transfer(staking.address, ether(500), { from: deployer }); 387 | const res = await staking.createWithdrawal(USER_ID, ether(30), { from: alice }); 388 | createdAt = await getResTimestamp(res); 389 | }); 390 | 391 | it('should allow the users admin key withdrawing all the pending deposit', async function() { 392 | expect(await cvpToken.balanceOf(sink)).to.be.equal('0'); 393 | expect(await staking.totalDeposit()).to.be.equal(ether('70')); 394 | 395 | let user = await staking.users(USER_ID); 396 | expect(user.deposit).to.be.equal(ether(70)); 397 | expect(user.pendingWithdrawal).to.be.equal(ether(30)); 398 | expect(user.pendingWithdrawalTimeout).to.be.equal(strSum(createdAt, WITHDRAWAL_TIMEOUT)); 399 | 400 | await time.increase(10); 401 | 402 | let res = await staking.createWithdrawal(USER_ID, ether(70), { from: alice }); 403 | createdAt = await getResTimestamp(res); 404 | 405 | user = await staking.users(USER_ID); 406 | expect(user.deposit).to.be.equal(ether(0)); 407 | expect(user.pendingWithdrawal).to.be.equal(ether(100)); 408 | expect(user.pendingWithdrawalTimeout).to.be.equal(strSum(createdAt, WITHDRAWAL_TIMEOUT)); 409 | 410 | await time.increase(WITHDRAWAL_TIMEOUT); 411 | res = await staking.executeWithdrawal(USER_ID, sink, { from: alice }); 412 | 413 | expect(await cvpToken.balanceOf(sink)).to.be.equal(ether('100')); 414 | expect(await staking.totalDeposit()).to.be.equal(ether('0')); 415 | 416 | expectEvent(res, 'ExecuteWithdrawal', { 417 | userId: '42', 418 | to: sink, 419 | pendingTimeout: strSum(createdAt, WITHDRAWAL_TIMEOUT), 420 | amount: ether(100), 421 | }); 422 | user = await staking.users(USER_ID); 423 | expect(user.deposit).to.be.equal(ether(0)); 424 | expect(user.pendingWithdrawal).to.be.equal(ether(0)); 425 | expect(user.pendingWithdrawalTimeout).to.be.equal(ether(0)); 426 | }); 427 | 428 | it('should deny non-admin withdrawing rewards', async function() { 429 | await expect(staking.executeWithdrawal(USER_ID, sink, { from: alicePoker })) 430 | .to.be.revertedWith('ONLY_ADMIN_ALLOWED'); 431 | }); 432 | 433 | it('should deny withdrawing 0 balance', async function() { 434 | await time.increase(WITHDRAWAL_TIMEOUT); 435 | await staking.executeWithdrawal(USER_ID, sink, { from: alice }); 436 | await expect(staking.executeWithdrawal(USER_ID, sink, { from: alice })) 437 | .to.be.revertedWith('NO_PENDING_WITHDRAWAL'); 438 | }); 439 | 440 | it('should deny withdrawing to 0 address', async function() { 441 | await expect(staking.executeWithdrawal(USER_ID, constants.ZERO_ADDRESS, { from: alice })) 442 | .to.be.revertedWith('CANT_WITHDRAW_TO_0'); 443 | }); 444 | }); 445 | }); 446 | 447 | describe('owner interface', () => { 448 | describe('setSlasher', () => { 449 | it('should allow the owner setting the value', async function() { 450 | await staking.setSlasher(charlie, { from: owner }); 451 | expect(await staking.slasher()).to.be.equal(charlie); 452 | }) 453 | 454 | it('should deny non-owner setting the value', async function() { 455 | await expect(staking.setSlasher(charlie, { from: alice })) 456 | .to.be.revertedWith('NOT_THE_OWNER'); 457 | }) 458 | }) 459 | 460 | describe('setSlashingPct', () => { 461 | it('should allow the owner setting the value', async function() { 462 | await staking.setSlashingPct(ether(40), ether(30), { from: owner }); 463 | expect(await staking.slasherSlashingRewardPct()).to.be.equal(ether(40)); 464 | expect(await staking.protocolSlashingRewardPct()).to.be.equal(ether(30)); 465 | }) 466 | 467 | it('should deny a slasher and the protocol reward more than 100%', async function() { 468 | await expect(staking.setSlashingPct(ether(50), ether(51), { from: owner })) 469 | .to.be.revertedWith('INVALID_SUM'); 470 | }) 471 | 472 | it('should deny non-owner setting the value', async function() { 473 | await expect(staking.setSlashingPct(ether(40), ether(30), { from: alice })) 474 | .to.be.revertedWith('NOT_THE_OWNER'); 475 | }) 476 | }); 477 | 478 | describe('setTimeouts', () => { 479 | it('should allow the owner setting the values', async function() { 480 | await staking.setTimeouts(888, 999, { from: owner }); 481 | expect(await staking.depositTimeout()).to.be.equal('888'); 482 | expect(await staking.withdrawalTimeout()).to.be.equal('999'); 483 | }) 484 | 485 | it('should deny non-owner setting the value', async function() { 486 | await expect(staking.setTimeouts(ether(40), ether(30), { from: alice })) 487 | .to.be.revertedWith('NOT_THE_OWNER'); 488 | }) 489 | }); 490 | 491 | describe('pause', () => { 492 | it('should allow the owner pausing the contract', async function() { 493 | expect(await staking.paused()).to.be.false; 494 | await staking.pause({ from: owner }); 495 | expect(await staking.paused()).to.be.true; 496 | }); 497 | 498 | it('should deny non-owner pausing the contract', async function() { 499 | await expect(staking.pause({ from: alice })) 500 | .to.be.revertedWith('NOT_THE_OWNER'); 501 | }); 502 | }) 503 | 504 | describe('unpause', () => { 505 | beforeEach(async function() { 506 | await staking.pause({ from: owner }); 507 | }); 508 | 509 | it('should allow the owner unpausing the contract', async function() { 510 | expect(await staking.paused()).to.be.true; 511 | await staking.unpause({ from: owner }); 512 | expect(await staking.paused()).to.be.false; 513 | }); 514 | 515 | it('should deny non-owner unpausing the contract', async function() { 516 | await expect(staking.unpause({ from: alice })) 517 | .to.be.revertedWith('NOT_THE_OWNER'); 518 | }); 519 | }) 520 | }); 521 | 522 | describe('setReporter', () => { 523 | beforeEach(async function() { 524 | const powerOracle = await MockOracle.new(cvpToken.address, 1); 525 | await powerOracle.addTokens(await getTokenConfigs(cvpToken.address)); 526 | await staking.setSlasher(powerOracle.address, { from: owner }); 527 | }); 528 | 529 | it('should allow setting reporter if there is another user with a higher deposit', async function() { 530 | await staking.stubSetUser(1, alice, alicePoker, ether(100), { from: bob }); 531 | await staking.stubSetUser(2, bob, bobPoker, ether(200), { from: bob }); 532 | 533 | await staking.stubSetReporter(1, ether(300)); 534 | expect(await staking.getHDHID()).to.be.equal('1'); 535 | expect(await staking.getHighestDeposit()).to.be.equal(ether(300)); 536 | 537 | await staking.setHDH(2); 538 | expect(await staking.getHDHID()).to.be.equal('2'); 539 | expect(await staking.getHighestDeposit()).to.be.equal(ether(200)); 540 | }); 541 | 542 | it('should deny setting reporter with not the highest deposit', async function() { 543 | await staking.stubSetUser(1, alice, alicePoker, ether(100), { from: bob }); 544 | await staking.stubSetUser(2, bob, bobPoker, ether(100), { from: bob }); 545 | 546 | await staking.stubSetReporter(1, ether(300)); 547 | 548 | await expect(staking.setHDH(2)) 549 | .to.be.revertedWith('INSUFFICIENT_CANDIDATE_DEPOSIT'); 550 | }); 551 | }); 552 | 553 | describe('slash', () => { 554 | const SLASHER_ID = '42'; 555 | const REPORTER_ID = '5'; 556 | 557 | beforeEach(async function() { 558 | await cvpToken.transfer(staking.address, ether(10000), { from: deployer }); 559 | 560 | await staking.stubSetUser(REPORTER_ID, alice, alicePoker, ether(500), { from: bob }); 561 | await staking.stubSetUser(SLASHER_ID, bob, bobPoker, ether(60), { from: bob }); 562 | await staking.stubSetTotalDeposit(ether(560), { from: bob }); 563 | await staking.stubSetReporter(REPORTER_ID, ether(600), { from: bob }); 564 | await staking.setSlasher(powerPoke, { from: owner }); 565 | }); 566 | 567 | it('should allow slashing current reporter', async function() { 568 | expect(await staking.getDepositOf(REPORTER_ID)).to.be.equal(ether(500)); 569 | expect(await staking.getDepositOf(SLASHER_ID)).to.be.equal(ether(60)); 570 | expect(await cvpToken.balanceOf(reservoir)).to.be.equal('0'); 571 | expect(await staking.totalDeposit()).to.be.equal(ether(560)); 572 | expect(await staking.slasherSlashingRewardPct()).to.be.equal(ether(5)); 573 | expect(await staking.protocolSlashingRewardPct()).to.be.equal(ether('1.5')); 574 | 575 | const res = await staking.slashHDH(SLASHER_ID, 4, { from: powerPoke }); 576 | expectEvent(res, 'Slash', { 577 | slasherId: SLASHER_ID, 578 | reporterId: REPORTER_ID, 579 | // 4 * 500 * 0.05 = 100 580 | slasherReward: ether(100), 581 | // 4 * 500 * 0.015 = 100 582 | reservoirReward: ether(30) 583 | }) 584 | 585 | expect(await staking.totalDeposit()).to.be.equal(ether(530)); 586 | // 500 - 100 - 30 = 370 587 | expect(await staking.getDepositOf(REPORTER_ID)).to.be.equal(ether(370)); 588 | // 100 + 100 = 200 589 | expect(await staking.getDepositOf(SLASHER_ID)).to.be.equal(ether(160)); 590 | expect(await cvpToken.balanceOf(reservoir)).to.be.equal(ether(30)); 591 | 592 | expect(await staking.getHDHID()).to.be.equal(REPORTER_ID); 593 | 594 | expectEvent(res, 'Slash', { 595 | slasherId: '42', 596 | reporterId: '5', 597 | // 500 * 5% * 4 = 25 * 4 = 100 598 | slasherReward: ether(100), 599 | // 500 * 1.5% * 4 = 7.5 * 4 = 30 600 | reservoirReward: ether(30) 601 | }) 602 | }); 603 | 604 | it('should change the reporterId to the slasher the reporters deposit becomes lesser', async function() { 605 | await staking.slashHDH(SLASHER_ID, 4, { from: powerPoke }); 606 | 607 | expect(await staking.getDepositOf(REPORTER_ID)).to.be.equal(ether(370)); 608 | expect(await staking.getDepositOf(SLASHER_ID)).to.be.equal(ether(160)); 609 | 610 | await staking.slashHDH(SLASHER_ID, 4, { from: powerPoke }); 611 | expect(await staking.getDepositOf(REPORTER_ID)).to.be.equal(ether(273.8)); 612 | expect(await staking.getDepositOf(SLASHER_ID)).to.be.equal(ether(234)); 613 | 614 | const res = await staking.slashHDH(SLASHER_ID, 4, { from: powerPoke }); 615 | expect(await staking.getDepositOf(REPORTER_ID)).to.be.equal(ether('202.612')); 616 | expect(await staking.getDepositOf(SLASHER_ID)).to.be.equal(ether('288.76')); 617 | 618 | expect(await staking.getHDHID()).to.be.equal(SLASHER_ID); 619 | 620 | expectEvent(res, 'ReporterChange', { 621 | prevId: REPORTER_ID, 622 | nextId: SLASHER_ID, 623 | highestDepositPrev: ether(600), 624 | actualDepositPrev: ether('202.612'), 625 | actualDepositNext: ether('288.76') 626 | }) 627 | }); 628 | 629 | it('should work correctly with 0 slasher/reservoir rewardPct values', async function() { 630 | await staking.setSlashingPct(ether(0), ether(0), { from: owner }); 631 | expect(await staking.slasherSlashingRewardPct()).to.be.equal(ether(0)); 632 | expect(await staking.protocolSlashingRewardPct()).to.be.equal(ether(0)); 633 | 634 | expect(await staking.getDepositOf(REPORTER_ID)).to.be.equal(ether(500)); 635 | expect(await staking.getDepositOf(SLASHER_ID)).to.be.equal(ether(60)); 636 | expect(await cvpToken.balanceOf(reservoir)).to.be.equal('0'); 637 | expect(await staking.totalDeposit()).to.be.equal(ether(560)); 638 | expect(await staking.slasherSlashingRewardPct()).to.be.equal(ether(0)); 639 | expect(await staking.protocolSlashingRewardPct()).to.be.equal(ether(0)); 640 | 641 | const res = await staking.slashHDH(SLASHER_ID, 4, { from: powerPoke }); 642 | expectEvent(res, 'Slash', { 643 | slasherId: SLASHER_ID, 644 | reporterId: REPORTER_ID, 645 | slasherReward: ether(0), 646 | reservoirReward: ether(0) 647 | }) 648 | 649 | expect(await staking.totalDeposit()).to.be.equal(ether(560)); 650 | expect(await staking.getDepositOf(REPORTER_ID)).to.be.equal(ether(500)); 651 | expect(await staking.getDepositOf(SLASHER_ID)).to.be.equal(ether(60)); 652 | expect(await cvpToken.balanceOf(reservoir)).to.be.equal(ether(0)); 653 | expect(await staking.getHDHID()).to.be.equal(REPORTER_ID); 654 | }); 655 | 656 | it('should deny slashing if the slasher deposit is not sufficient', async function() { 657 | await staking.stubSetUser(SLASHER_ID, bob, bobPoker, ether(40), { from: bob }); 658 | await expect(staking.slashHDH(SLASHER_ID, 100, { from: powerPoke })) 659 | .to.be.revertedWith('INSUFFICIENT_HDH_DEPOSIT'); 660 | }); 661 | 662 | it('should deny non-powerPoke calling the method', async function() { 663 | await expect(staking.slashHDH(SLASHER_ID, 4, { from: owner })) 664 | .to.be.revertedWith('ONLY_SLASHER_ALLOWED'); 665 | }); 666 | }) 667 | 668 | describe('viewers', () => { 669 | beforeEach(async function() { 670 | await staking.stubSetReporter(3, ether(50)); 671 | 672 | // it's ok to use the same keys for different users 673 | await staking.stubSetUser(1, alice, alicePoker, ether(30)); 674 | await staking.stubSetUser(2, bob, bobPoker, ether(50)); 675 | await staking.stubSetUser(3, charlie, charliePoker, ether(100)); 676 | }); 677 | 678 | describe('getUserStatus', () => { 679 | it('should respond with UNAUTHORIZED if there is not enough deposit', async function() { 680 | expect(await staking.getUserStatus(1, alicePoker, MINIMAL_SLASHING_DEPOSIT)).to.be.equal(USER_STATUS.UNAUTHORIZED); 681 | }); 682 | 683 | it('should respond with UNAUTHORIZED if there is no match between a poker key and a user id', async function() { 684 | expect(await staking.getUserStatus(2, alicePoker, MINIMAL_SLASHING_DEPOSIT)).to.be.equal(USER_STATUS.UNAUTHORIZED); 685 | }); 686 | 687 | it('should respond with HDH if there is enough deposit, but not a reporter', async function() { 688 | expect(await staking.getUserStatus(2, bobPoker, MINIMAL_SLASHING_DEPOSIT)).to.be.equal(USER_STATUS.HDH); 689 | }); 690 | 691 | it('should respond with MEMBER if there is enough deposit and is a reporter', async function() { 692 | expect(await staking.getUserStatus(3, charliePoker, MINIMAL_SLASHING_DEPOSIT)).to.be.equal(USER_STATUS.MEMBER); 693 | }); 694 | 695 | it('should respond with UNAUTHORIZED if there is no match between a reporter and a user id', async function() { 696 | expect(await staking.getUserStatus(3, alicePoker, MINIMAL_SLASHING_DEPOSIT)).to.be.equal(USER_STATUS.UNAUTHORIZED); 697 | }); 698 | }) 699 | 700 | describe('authorizeHDH', () => { 701 | it('should authorize a valid reporter', async function() { 702 | await staking.authorizeHDH(3, charliePoker); 703 | }); 704 | 705 | it('should not authorize an invalid reporter', async function() { 706 | await expect(staking.authorizeHDH(2, bobPoker)) 707 | .to.be.revertedWith('NOT_HDH'); 708 | }); 709 | 710 | it('should not authorize a valid reporter with an invalid poker key', async function() { 711 | await expect(staking.authorizeHDH(3, bobPoker)) 712 | .to.be.revertedWith('INVALID_POKER_KEY'); 713 | }); 714 | }) 715 | 716 | describe('authorizeNonHDH', () => { 717 | it('should authorize a valid non-HDH member', async function() { 718 | await staking.authorizeNonHDH(2, bobPoker, MINIMAL_SLASHING_DEPOSIT); 719 | }); 720 | 721 | it('should not authorize the HDH member', async function() { 722 | await expect(staking.authorizeNonHDH(3, charliePoker, MINIMAL_SLASHING_DEPOSIT)) 723 | .to.be.revertedWith('IS_HDH'); 724 | }); 725 | 726 | it('should not authorize a valid non-HDH member with an invalid poker key', async function() { 727 | await expect(staking.authorizeNonHDH(1, bobPoker, 0)) 728 | .to.be.revertedWith('INVALID_POKER_KEY'); 729 | }); 730 | }) 731 | 732 | describe('authorizeMember', () => { 733 | it('should authorize a valid slasher', async function() { 734 | await staking.authorizeMember(2, bobPoker, MINIMAL_SLASHING_DEPOSIT); 735 | }); 736 | 737 | it('should not authorize an insufficient deposit', async function() { 738 | await expect(staking.authorizeMember(1, alicePoker, MINIMAL_SLASHING_DEPOSIT)) 739 | .to.be.revertedWith('INSUFFICIENT_DEPOSIT'); 740 | }); 741 | 742 | it('should not authorize a valid slasher with an invalid poker key', async function() { 743 | await expect(staking.authorizeMember(2, alicePoker, MINIMAL_SLASHING_DEPOSIT)) 744 | .to.be.revertedWith('INVALID_POKER_KEY'); 745 | }); 746 | }) 747 | }); 748 | }); 749 | -------------------------------------------------------------------------------- /test/builders.js: -------------------------------------------------------------------------------- 1 | const { fixed } = require('./helpers'); 2 | 3 | const MockUniswapTokenPair = artifacts.require('MockUniswapTokenPair'); 4 | 5 | async function buildPair() { 6 | return await MockUniswapTokenPair.new( 7 | fixed(1.8e12), 8 | fixed(8.2e18), 9 | fixed(1.6e9), 10 | fixed(1.19e50), 11 | fixed(5.8e30) 12 | ); 13 | } 14 | 15 | /** 16 | * Build Uniswap CVP pair 17 | * @param {string} timestamp 18 | * @returns {Promise} 19 | */ 20 | async function buildCvpPair(timestamp) { 21 | return MockUniswapTokenPair.new( 22 | // reserve0_ 23 | '447524245108904579507942', 24 | // reserve1_ 25 | '2375909307458759621213', 26 | // blockTimestampLast_ 27 | // '1602266545', 28 | timestamp, 29 | // price0CumulativeLast_ 30 | '203775841804087426407614127214505850328', 31 | // price1CumulativeLast_ 32 | '2252004857118134099488260334514310055795531' 33 | ); 34 | } 35 | 36 | /** 37 | * Build Uniswap CVP pair 38 | * @param {string} timestamp 39 | * @returns {Promise} 40 | */ 41 | async function buildUsdcEth(timestamp) { 42 | return MockUniswapTokenPair.new( 43 | // reserve0_ 44 | '266500031330401', 45 | // reserve1_ 46 | '731038125338232251226332', 47 | // blockTimestampLast_ 48 | // '1602277749', 49 | timestamp, 50 | // price0CumulativeLast_ 51 | '253482859812666220342361026802903530871862032089570', 52 | // price1CumulativeLast_ 53 | '21065123404263661723452955314367' 54 | ); 55 | } 56 | 57 | module.exports = { 58 | buildPair, 59 | buildCvpPair, 60 | buildUsdcEth 61 | }; 62 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | const TruffleContract = require('@nomiclabs/truffle-contract'); 2 | const { ether: etherBN, expectEvent } = require('@openzeppelin/test-helpers'); 3 | const { promisify } = require('util'); 4 | const BigNumber = require('bignumber.js') 5 | const fs = require('fs'); 6 | 7 | const AdminUpgradeabilityProxyArtifact = require('@openzeppelin/upgrades-core/artifacts/AdminUpgradeabilityProxy.json'); 8 | const ProxyAdminArtifact = require('@openzeppelin/upgrades-core/artifacts/ProxyAdmin.json'); 9 | const template = artifacts.require('PowerOracle'); 10 | const AdminUpgradeabilityProxy = TruffleContract(AdminUpgradeabilityProxyArtifact); 11 | const ProxyAdmin = TruffleContract(ProxyAdminArtifact); 12 | 13 | AdminUpgradeabilityProxy.setProvider(template.currentProvider); 14 | AdminUpgradeabilityProxy.defaults(template.class_defaults); 15 | ProxyAdmin.setProvider(template.currentProvider); 16 | ProxyAdmin.defaults(template.class_defaults); 17 | 18 | let proxyAdmin; 19 | 20 | /** 21 | * Deploys a proxied contract 22 | * 23 | * @param contract Truffle Contract 24 | * @param {string[]} constructorArgs 25 | * @param {string[]} initializerArgs 26 | * @param {object} opts 27 | * @param {string} opts.deployer 28 | * @param {string} opts.initializer 29 | * @param {string} opts.proxyAdminOwner 30 | * @returns {Promise} 31 | */ 32 | async function deployProxied( 33 | contract, 34 | constructorArgs = [], 35 | initializerArgs = [], 36 | opts = {} 37 | ) { 38 | const impl = opts.implementation ? await contract.at(opts.implementation) : await contract.new(...constructorArgs); 39 | const adminContract = await createOrGetProxyAdmin(opts.proxyAdminOwner); 40 | const data = getInitializerData(impl, initializerArgs, opts.initializer); 41 | const proxy = await AdminUpgradeabilityProxy.new(impl.address, adminContract.address, data); 42 | const instance = await contract.at(proxy.address); 43 | 44 | instance.proxy = proxy; 45 | instance.initialImplementation = impl; 46 | instance.adminContract = adminContract; 47 | 48 | return instance; 49 | } 50 | 51 | /** 52 | * Creates and returns ProxyAdmin contract 53 | * @param {string} proxyOwner 54 | * @returns {Promise} 55 | */ 56 | async function createOrGetProxyAdmin(proxyOwner) { 57 | if (!proxyAdmin) { 58 | proxyAdmin = await ProxyAdmin.new(); 59 | await proxyAdmin.transferOwnership(proxyOwner); 60 | } 61 | return proxyAdmin; 62 | } 63 | 64 | 65 | function getInitializerData(impl, args, initializer) { 66 | const allowNoInitialization = initializer === undefined && args.length === 0; 67 | initializer = initializer || 'initialize'; 68 | 69 | if (initializer in impl.contract.methods) { 70 | return impl.contract.methods[initializer](...args).encodeABI(); 71 | } else if (allowNoInitialization) { 72 | return '0x'; 73 | } else { 74 | throw new Error(`Contract ${impl.name} does not have a function \`${initializer}\``); 75 | } 76 | } 77 | 78 | function ether(value) { 79 | return etherBN(String(value)).toString(); 80 | } 81 | 82 | function tether(value) { 83 | return web3.utils.toWei(value, 'tether').toString(); 84 | } 85 | 86 | function fromWei(value, to = 'ether') { 87 | return web3.utils.fromWei(value, to); 88 | } 89 | 90 | function mwei(value) { 91 | return web3.utils.toWei(value, 'mwei').toString(); 92 | } 93 | 94 | function gwei(value) { 95 | return web3.utils.toWei(value.toString(), 'gwei').toString(); 96 | } 97 | 98 | /** 99 | * Finds a first event/arg occurrence and returns a value 100 | * @param {object} receipt 101 | * @param {object[]} receipt.logs 102 | * @param {string} eventName 103 | * @param {string} argName 104 | * @returns {any} 105 | */ 106 | function getEventArg(receipt, eventName, argName) { 107 | expectEvent(receipt, eventName); 108 | const logs = receipt.logs; 109 | for (let i = 0; i < logs.length; i++) { 110 | const event = logs[i]; 111 | if (event.event === eventName) { 112 | if (argName in event.args) { 113 | return event.args[argName]; 114 | } 115 | 116 | throw new Error(`helpers.js:getEventArgs: ${eventName} argument ${argName} missing`); 117 | } 118 | } 119 | throw new Error(`helpers.js:getEventArgs: Event ${eventName} not found`); 120 | } 121 | 122 | /** 123 | * Rewinds ganache by n blocks 124 | * @param {number} n 125 | * @returns {Promise} 126 | */ 127 | async function advanceBlocks(n) { 128 | // eslint-disable-next-line no-undef 129 | // const heck = web3.currentProvider.send.bind(web3.currentProvider, ); 130 | // const heck = new Promise((resolve, reject) => { 131 | const send = promisify(web3.currentProvider.send).bind(web3.currentProvider); 132 | const requests = []; 133 | for (let i = 0; i < n; i++) { 134 | requests.push(send({ 135 | jsonrpc: '2.0', 136 | method: 'evm_mine', 137 | id: `${new Date().getTime()}-${Math.random()}`, 138 | })); 139 | } 140 | await Promise.all(requests); 141 | } 142 | 143 | /** 144 | * Fetches logs of a given contract for a given tx, 145 | * since Truffle provides logs for a calle contract only. 146 | * @param {TruffleContract} contract 147 | * @param {object} receipt 148 | * @param {string} receipt.tx 149 | * @returns {Promise<{object}>} 150 | */ 151 | async function fetchLogs(contract, receipt) { 152 | const res = await web3.eth.getTransactionReceipt(receipt.tx); 153 | return contract.decodeLogs(res.logs); 154 | } 155 | 156 | async function getResTimestamp(res) { 157 | return (await web3.eth.getBlock(res.receipt.blockNumber)).timestamp.toString(); 158 | } 159 | 160 | /** 161 | * Shrinks function signature from ABI-encoded revert string. 162 | * @param value 163 | * @returns {string} 164 | */ 165 | function decodeRevertBytes(value) { 166 | return web3.eth.abi.decodeParameter('string', `0x${value.substring(10)}`); 167 | } 168 | 169 | /** 170 | * Splits calldata into a signature and arguments. 171 | * @param {string} data 172 | * @returns {(string)[]} 173 | */ 174 | function splitCalldata(data) { 175 | return [data.substring(0, 10), `0x${data.substring(10)}`] 176 | } 177 | 178 | /** 179 | * Makes operations with K-s more convenient. 180 | * @param v 181 | * @returns {string} 182 | */ 183 | function kether(v) { 184 | return ether(v * 1000); 185 | } 186 | 187 | function address(n) { 188 | return web3.utils.toChecksumAddress(`0x${n.toString(16).padStart(40, '0')}`); 189 | } 190 | 191 | function keccak256(str) { 192 | return web3.utils.keccak256(str); 193 | } 194 | 195 | function uint256(int) { 196 | return web3.eth.abi.encodeParameter('uint256', int); 197 | } 198 | 199 | function uint(n) { 200 | return web3.utils.toBN(n).toString(); 201 | } 202 | 203 | function toInt(n) { 204 | return parseInt(n, 10); 205 | } 206 | 207 | async function ethUsed(web3, receipt) { 208 | const tx = await web3.eth.getTransaction(receipt.transactionHash); 209 | return fromWei(new BigNumber(receipt.gasUsed.toString()).multipliedBy(new BigNumber(tx.gasPrice.toString())).toString()); 210 | } 211 | 212 | function strSum(a, b) { 213 | return String(toInt(a) + toInt(b)); 214 | } 215 | 216 | const fixed = num => { 217 | return (new BigNumber(num).toFixed()); 218 | }; 219 | 220 | async function forkContractUpgrade(ethers, adminAddress, proxyAdminAddress, proxyAddress, implAddress) { 221 | const iface = new ethers.utils.Interface(['function upgrade(address proxy, address impl)']); 222 | 223 | await ethers.provider.getSigner().sendTransaction({ 224 | to: adminAddress, 225 | value: '0x' + new BigNumber(ether('1')).toString(16) 226 | }) 227 | 228 | await ethers.provider.send('hardhat_impersonateAccount', [adminAddress]); 229 | 230 | await ethers.provider.getSigner(adminAddress).sendTransaction({ 231 | to: proxyAdminAddress, 232 | data: iface.encodeFunctionData('upgrade', [proxyAddress, implAddress]) 233 | }) 234 | } 235 | 236 | async function increaseTime(ethers, time) { 237 | return ethers.provider.send('evm_increaseTime', [time]); 238 | } 239 | 240 | async function deployAndSaveArgs(Contract, args) { 241 | const newInstance = await Contract.new.apply(Contract, args); 242 | fs.writeFileSync( 243 | `./tmp/${newInstance.address}-args.js`, 244 | `module.exports = ${JSON.stringify(args, null, 2)}` 245 | ); 246 | return newInstance; 247 | } 248 | 249 | async function impersonateAccount(ethers, adminAddress) { 250 | await ethers.provider.getSigner().sendTransaction({ 251 | to: adminAddress, 252 | value: '0x' + new BigNumber(ether('1')).toString(16) 253 | }) 254 | 255 | await ethers.provider.send('hardhat_impersonateAccount', [adminAddress]); 256 | } 257 | 258 | module.exports = { 259 | advanceBlocks, 260 | createOrGetProxyAdmin, 261 | deployProxied, 262 | ether, 263 | fromWei, 264 | mwei, 265 | gwei, 266 | tether, 267 | getEventArg, 268 | splitCalldata, 269 | fetchLogs, 270 | getResTimestamp, 271 | decodeRevertBytes, 272 | kether, 273 | address, 274 | keccak256, 275 | uint256, 276 | uint, 277 | toInt, 278 | strSum, 279 | fixed, 280 | ethUsed, 281 | forkContractUpgrade, 282 | deployAndSaveArgs, 283 | increaseTime, 284 | impersonateAccount 285 | } 286 | -------------------------------------------------------------------------------- /test/localHelpers.js: -------------------------------------------------------------------------------- 1 | const { address, keccak256, ether, uint } = require('./helpers'); 2 | const { buildPair } = require('./builders'); 3 | 4 | const PriceSource = { 5 | FIXED_USD: 0, 6 | REPORTER: 1 7 | }; 8 | 9 | const underlyings = { 10 | ETH: address(111), 11 | DAI: address(222), 12 | REP: address(333), 13 | USDT: address(444), 14 | SAI: address(555), 15 | WBTC: address(666), 16 | CVP: address(777), 17 | MKR: address(888), 18 | UNI: address(999), 19 | }; 20 | 21 | let mockPair; 22 | async function getPairMock() { 23 | if (!mockPair) { 24 | mockPair = await buildPair(); 25 | } 26 | return mockPair; 27 | } 28 | 29 | async function getTokenConfigs(cvpAddress) { 30 | const mockPair = await getPairMock(); 31 | 32 | return [ 33 | {token: underlyings.ETH, symbol: 'ETH', basic: {symbolHash: keccak256('ETH'), baseUnit: ether(1), priceSource: PriceSource.REPORTER, fixedPrice: 0, active: 2}, update: {uniswapMarket: mockPair.address, isUniswapReversed: true}}, 34 | {token: cvpAddress, symbol: 'CVP', basic: {symbolHash: keccak256('CVP'), baseUnit: ether(1), priceSource: PriceSource.REPORTER, fixedPrice: 0, active: 2}, update: {uniswapMarket: mockPair.address, isUniswapReversed: false}}, 35 | {token: underlyings.DAI, symbol: 'DAI', basic: {symbolHash: keccak256('DAI'), baseUnit: ether(1), priceSource: PriceSource.REPORTER, fixedPrice: 0, active: 2}, update: {uniswapMarket: mockPair.address, isUniswapReversed: false}}, 36 | {token: underlyings.REP, symbol: 'REP', basic: {symbolHash: keccak256('REP'), baseUnit: ether(1), priceSource: PriceSource.REPORTER, fixedPrice: 0, active: 2}, update: {uniswapMarket: mockPair.address, isUniswapReversed: false}}, 37 | {token: underlyings.USDT, symbol: 'USDT', basic: {symbolHash: keccak256('USDT'), baseUnit: uint(1e6), priceSource: PriceSource.FIXED_USD, fixedPrice: uint(1e6), active: 2}, update: {uniswapMarket: address(0), isUniswapReversed: false}}, 38 | {token: underlyings.WBTC, symbol: 'BTC', basic: {symbolHash: keccak256('BTC'), baseUnit: uint(1e8), priceSource: PriceSource.REPORTER, fixedPrice: 0, active: 2}, update: {uniswapMarket: mockPair.address, isUniswapReversed: false}}, 39 | ]; 40 | } 41 | 42 | async function getAnotherTokenConfigs() { 43 | const mockPair = await getPairMock(); 44 | return [ 45 | {token: underlyings.MKR, symbol: 'MKR', basic: {symbolHash: keccak256('MKR'), baseUnit: ether(1), priceSource: PriceSource.REPORTER, fixedPrice: 0, active: 2}, update: {uniswapMarket: mockPair.address, isUniswapReversed: true}}, 46 | {token: underlyings.UNI, symbol: 'UNI', basic: {symbolHash: keccak256('UNI'), baseUnit: ether(1), priceSource: PriceSource.REPORTER, fixedPrice: 0, active: 2}, update: {uniswapMarket: mockPair.address, isUniswapReversed: true}}, 47 | ]; 48 | } 49 | 50 | module.exports = { getTokenConfigs, getAnotherTokenConfigs } 51 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | compilers: { 3 | solc: { 4 | version: 'native', 5 | settings: { 6 | optimizer: { 7 | enabled: true, 8 | runs: 200 9 | } 10 | } 11 | } 12 | } 13 | }; 14 | --------------------------------------------------------------------------------