diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f0d30a2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,43 @@ +{ + "name": "smart-contract-evm", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smart-contract-evm", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@pythnetwork/pyth-sdk-solidity": "^2.4.1", + "dotenv": "^16.4.1", + "minimist": "^1.2.8" + }, + "devDependencies": {} + }, + "node_modules/@pythnetwork/pyth-sdk-solidity": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-solidity/-/pyth-sdk-solidity-2.4.1.tgz", + "integrity": "sha512-QNrdtv+YiEszz0hvBOWXaJ3PeKJfcSnQnB9GuSZOK1WVuJxNtPTd1Hc2hrpxPm0B4SBKwSSo10I0jb4GXHi42g==" + }, + "node_modules/dotenv": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", + "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..399d627 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "smart-contract-evm", + "version": "1.0.0", + "description": "JOJO is a decentralized perpetual contract exchange based on an off-chain matching system that can be divided into three key components: trading, collateral lending, and funding rate arbitrage.", + "main": "index.js", + "directories": { + "lib": "lib", + "test": "test" + }, + "dependencies": { + "@pythnetwork/pyth-sdk-solidity": "^2.4.1", + "dotenv": "^16.4.1", + "minimist": "^1.2.8" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/remappings.txt b/remappings.txt index 90b2e67..db2c590 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,3 @@ @openzeppelin/=lib/openzeppelin-contracts/ +@pythnetwork/pyth-sdk-solidity/=node_modules/@pythnetwork/pyth-sdk-solidity forge-std/=lib/forge-std/src/ diff --git a/src/oracle/OracleAdaptor.sol b/src/oracle/OracleAdaptor.sol index 1814892..a7fe6ea 100644 --- a/src/oracle/OracleAdaptor.sol +++ b/src/oracle/OracleAdaptor.sol @@ -7,8 +7,6 @@ pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; -import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; import "../interfaces/internal/IChainlink.sol"; contract OracleAdaptor is Ownable { @@ -17,31 +15,44 @@ contract OracleAdaptor is Ownable { uint256 public immutable usdcHeartbeat; address public immutable usdcSource; address public immutable chainlink; - bytes32 public immutable priceId; + uint256 public roundId; uint256 public price; uint256 public priceThreshold; - IPyth public pyth; + bool public isSelfOracle; + + // Align with chainlink + event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt); event UpdateThreshold(uint256 oldThreshold, uint256 newThreshold); constructor( address _chainlink, - address _pythContract, uint256 _decimalsCorrection, uint256 _heartbeatInterval, uint256 _usdcHeartbeat, address _usdcSource, - uint256 _priceThreshold, - bytes32 _priceId + uint256 _priceThreshold ) { chainlink = _chainlink; - pyth = IPyth(_pythContract); decimalsCorrection = 10 ** _decimalsCorrection; heartbeatInterval = _heartbeatInterval; usdcHeartbeat = _usdcHeartbeat; usdcSource = _usdcSource; priceThreshold = _priceThreshold; - priceId = _priceId; + } + + function setMarkPrice(uint256 newPrice) external onlyOwner { + price = newPrice; + emit AnswerUpdated(SafeCast.toInt256(price), roundId, block.timestamp); + roundId += 1; + } + + function turnOnJOJOOracle() external onlyOwner { + isSelfOracle = true; + } + + function turnOffJOJOOracle() external onlyOwner { + isSelfOracle = false; } function updateThreshold(uint256 newPriceThreshold) external onlyOwner { @@ -57,29 +68,26 @@ contract OracleAdaptor is Ownable { require(block.timestamp - updatedAt <= heartbeatInterval, "ORACLE_HEARTBEAT_FAILED"); require(block.timestamp - usdcUpdatedAt <= usdcHeartbeat, "USDC_ORACLE_HEARTBEAT_FAILED"); uint256 tokenPrice = (SafeCast.toUint256(rawPrice) * 1e8) / SafeCast.toUint256(usdcPrice); - return tokenPrice; + return (tokenPrice * 1e18) / decimalsCorrection; } function getPrice() internal view returns (uint256) { uint256 chainLinkPrice = getChainLinkPrice(); - try pyth.getPrice(priceId) returns (PythStructs.Price memory pythPriceStruct) { - uint256 pythPrice = SafeCast.toUint256(pythPriceStruct.price); - uint256 diff = pythPrice >= chainLinkPrice ? pythPrice - chainLinkPrice : chainLinkPrice - pythPrice; - if ((diff * 1e18) / chainLinkPrice <= priceThreshold) { - return chainLinkPrice; - } else { - return pythPrice; - } - } catch { + if (isSelfOracle) { + uint256 JOJOPrice = price; + uint256 diff = JOJOPrice >= chainLinkPrice ? JOJOPrice - chainLinkPrice : chainLinkPrice - JOJOPrice; + require((diff * 1e18) / chainLinkPrice <= priceThreshold, "deviation is too big"); + return price; + } else { return chainLinkPrice; } } function getMarkPrice() external view returns (uint256) { - return (getPrice() * 1e18) / decimalsCorrection; + return getPrice(); } function getAssetPrice() external view returns (uint256) { - return (getPrice() * 1e18) / decimalsCorrection; + return getPrice(); } -} +} \ No newline at end of file diff --git a/src/oracle/PythOracleAdaptor.sol b/src/oracle/PythOracleAdaptor.sol index 7e53790..1814892 100644 --- a/src/oracle/PythOracleAdaptor.sol +++ b/src/oracle/PythOracleAdaptor.sol @@ -6,27 +6,80 @@ pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; +import "../interfaces/internal/IChainlink.sol"; -contract PythOracleAdaptor is Ownable { +contract OracleAdaptor is Ownable { + uint256 public immutable decimalsCorrection; + uint256 public immutable heartbeatInterval; + uint256 public immutable usdcHeartbeat; + address public immutable usdcSource; + address public immutable chainlink; + bytes32 public immutable priceId; + uint256 public price; + uint256 public priceThreshold; IPyth public pyth; - constructor(address _pythContract) { + event UpdateThreshold(uint256 oldThreshold, uint256 newThreshold); + + constructor( + address _chainlink, + address _pythContract, + uint256 _decimalsCorrection, + uint256 _heartbeatInterval, + uint256 _usdcHeartbeat, + address _usdcSource, + uint256 _priceThreshold, + bytes32 _priceId + ) { + chainlink = _chainlink; pyth = IPyth(_pythContract); + decimalsCorrection = 10 ** _decimalsCorrection; + heartbeatInterval = _heartbeatInterval; + usdcHeartbeat = _usdcHeartbeat; + usdcSource = _usdcSource; + priceThreshold = _priceThreshold; + priceId = _priceId; + } + + function updateThreshold(uint256 newPriceThreshold) external onlyOwner { + priceThreshold = newPriceThreshold; + emit UpdateThreshold(priceThreshold, newPriceThreshold); + } + + function getChainLinkPrice() public view returns (uint256) { + int256 rawPrice; + uint256 updatedAt; + (, rawPrice,, updatedAt,) = IChainlink(chainlink).latestRoundData(); + (, int256 usdcPrice,, uint256 usdcUpdatedAt,) = IChainlink(usdcSource).latestRoundData(); + require(block.timestamp - updatedAt <= heartbeatInterval, "ORACLE_HEARTBEAT_FAILED"); + require(block.timestamp - usdcUpdatedAt <= usdcHeartbeat, "USDC_ORACLE_HEARTBEAT_FAILED"); + uint256 tokenPrice = (SafeCast.toUint256(rawPrice) * 1e8) / SafeCast.toUint256(usdcPrice); + return tokenPrice; + } + + function getPrice() internal view returns (uint256) { + uint256 chainLinkPrice = getChainLinkPrice(); + try pyth.getPrice(priceId) returns (PythStructs.Price memory pythPriceStruct) { + uint256 pythPrice = SafeCast.toUint256(pythPriceStruct.price); + uint256 diff = pythPrice >= chainLinkPrice ? pythPrice - chainLinkPrice : chainLinkPrice - pythPrice; + if ((diff * 1e18) / chainLinkPrice <= priceThreshold) { + return chainLinkPrice; + } else { + return pythPrice; + } + } catch { + return chainLinkPrice; + } + } + + function getMarkPrice() external view returns (uint256) { + return (getPrice() * 1e18) / decimalsCorrection; } - function setMarkPrice( - bytes[] calldata updateData, - bytes32[] calldata priceIds, - uint64[] calldata publishTimes - ) - external - payable - onlyOwner - { - // Update the on-chain Pyth price(s) - uint256 fee = pyth.getUpdateFee(updateData); - pyth.updatePriceFeedsIfNecessary{ value: fee }(updateData, priceIds, publishTimes); + function getAssetPrice() external view returns (uint256) { + return (getPrice() * 1e18) / decimalsCorrection; } } diff --git a/src/oracle/PythOracleUpdate.sol b/src/oracle/PythOracleUpdate.sol new file mode 100644 index 0000000..7e53790 --- /dev/null +++ b/src/oracle/PythOracleUpdate.sol @@ -0,0 +1,32 @@ +/* + Copyright 2022 JOJO Exchange + SPDX-License-Identifier: BUSL-1.1 +*/ + +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; +import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; + +contract PythOracleAdaptor is Ownable { + IPyth public pyth; + + constructor(address _pythContract) { + pyth = IPyth(_pythContract); + } + + function setMarkPrice( + bytes[] calldata updateData, + bytes32[] calldata priceIds, + uint64[] calldata publishTimes + ) + external + payable + onlyOwner + { + // Update the on-chain Pyth price(s) + uint256 fee = pyth.getUpdateFee(updateData); + pyth.updatePriceFeedsIfNecessary{ value: fee }(updateData, priceIds, publishTimes); + } +}