From b6adfb265a21be312b5e639498c0ea29e1572163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Tue, 29 Oct 2024 11:13:52 +0100 Subject: [PATCH] chore: lighten WitPriceFeedsUpgradable --- contracts/apps/WitPriceFeedsUpgradable.sol | 479 +++++++----------- contracts/data/WitPriceFeedsData.sol | 73 --- contracts/data/WitPriceFeedsDataLib.sol | 442 ++++++++++++++++ contracts/interfaces/IWitFeedsAdmin.sol | 7 +- contracts/interfaces/IWitFeedsEvents.sol | 4 +- contracts/libs/WitPriceFeedsLib.sol | 96 ---- contracts/mockups/WitPriceFeedsSolverBase.sol | 9 +- migrations/scripts/3_framework.js | 2 +- scripts/verify-libs.js | 2 +- settings/artifacts.js | 2 +- settings/specs.js | 2 +- 11 files changed, 645 insertions(+), 473 deletions(-) delete mode 100644 contracts/data/WitPriceFeedsData.sol create mode 100644 contracts/data/WitPriceFeedsDataLib.sol delete mode 100644 contracts/libs/WitPriceFeedsLib.sol diff --git a/contracts/apps/WitPriceFeedsUpgradable.sol b/contracts/apps/WitPriceFeedsUpgradable.sol index 8b59e3f4..e2091fde 100644 --- a/contracts/apps/WitPriceFeedsUpgradable.sol +++ b/contracts/apps/WitPriceFeedsUpgradable.sol @@ -3,12 +3,14 @@ pragma solidity >=0.8.0 <0.9.0; import "../WitPriceFeeds.sol"; import "../core/WitnetUpgradableBase.sol"; -import "../data/WitPriceFeedsData.sol"; + +import "../data/WitPriceFeedsDataLib.sol"; + import "../interfaces/IWitFeedsAdmin.sol"; import "../interfaces/IWitFeedsLegacy.sol"; import "../interfaces/IWitPriceFeedsSolverFactory.sol"; import "../interfaces/IWitOracleLegacy.sol"; -import "../libs/WitPriceFeedsLib.sol"; + import "../patterns/Ownable2Step.sol"; /// @title WitPriceFeeds: Price Feeds live repository reliant on the Wit/oracle blockchain. @@ -18,7 +20,6 @@ contract WitPriceFeedsUpgradable is Ownable2Step, WitPriceFeeds, - WitPriceFeedsData, WitnetUpgradableBase, IWitFeedsAdmin, IWitFeedsLegacy, @@ -76,7 +77,7 @@ contract WitPriceFeedsUpgradable msg.sig == IWitPriceFeedsSolver.solve.selector && msg.sender == address(this) ) { - address _solver = __records_(bytes4(bytes8(msg.data) << 32)).solver; + address _solver = WitPriceFeedsDataLib.seekRecord(bytes4(bytes8(msg.data) << 32)).solver; _require( _solver != address(0), "unsettled solver" @@ -111,6 +112,7 @@ contract WitPriceFeedsUpgradable }); // settle default base fee overhead percentage __baseFeeOverheadPercentage = 10; + } else { // otherwise, store beacon read from _initData, if any if (_initData.length > 0) { @@ -139,8 +141,30 @@ contract WitPriceFeedsUpgradable // ================================================================================================================ - // --- Implements 'IFeeds' ---------------------------------------------------------------------------------------- + // --- Implements 'IWitFeeds' ------------------------------------------------------------------------------------- + + function defaultRadonSLA() + override + public view + returns (Witnet.QuerySLA memory) + { + return __defaultRadonSLA; + } + function estimateUpdateBaseFee(uint256 _evmGasPrice) virtual override public view returns (uint256) { + return estimateUpdateRequestFee(_evmGasPrice); + } + + function estimateUpdateRequestFee(uint256 _evmGasPrice) + virtual override + public view + returns (uint) + { + return (IWitOracleLegacy(address(witOracle)).estimateBaseFee(_evmGasPrice, 32) + * (100 + __baseFeeOverheadPercentage) + ) / 100; + } + /// @notice Returns unique hash determined by the combination of data sources being used /// @notice on non-routed price feeds, and dependencies of routed price feeds. /// @dev Ergo, `footprint()` changes if any data source is modified, or the dependecy tree @@ -163,7 +187,7 @@ contract WitPriceFeedsUpgradable public pure returns (bytes4) { - return bytes4(keccak256(bytes(caption))); + return WitPriceFeedsDataLib.hash(caption); } function lookupCaption(bytes4 feedId) @@ -171,7 +195,7 @@ contract WitPriceFeedsUpgradable public view returns (string memory) { - return __records_(feedId).caption; + return WitPriceFeedsDataLib.seekRecord(feedId).caption; } function supportedFeeds() @@ -179,14 +203,7 @@ contract WitPriceFeedsUpgradable external view returns (bytes4[] memory _ids, string[] memory _captions, bytes32[] memory _solvers) { - _ids = __storage().ids; - _captions = new string[](_ids.length); - _solvers = new bytes32[](_ids.length); - for (uint _ix = 0; _ix < _ids.length; _ix ++) { - Record storage __record = __records_(_ids[_ix]); - _captions[_ix] = __record.caption; - _solvers[_ix] = address(__record.solver) == address(0) ? __record.radHash : bytes32(bytes20(__record.solver)); - } + return WitPriceFeedsDataLib.supportedFeeds(); } function supportsCaption(string calldata caption) @@ -195,7 +212,7 @@ contract WitPriceFeedsUpgradable returns (bool) { bytes4 feedId = hash(caption); - return hash(__records_(feedId).caption) == feedId; + return hash(WitPriceFeedsDataLib.seekRecord(feedId).caption) == feedId; } function totalFeeds() @@ -206,51 +223,27 @@ contract WitPriceFeedsUpgradable return __storage().ids.length; } - - // ================================================================================================================ - // --- Implements 'IWitFeeds' ---------------------------------------------------------------------------------- - - function defaultRadonSLA() - override - public view - returns (Witnet.QuerySLA memory) - { - return __defaultRadonSLA; - } - - function estimateUpdateBaseFee(uint256 _evmGasPrice) virtual override public view returns (uint256) { - return estimateUpdateRequestFee(_evmGasPrice); - } - - function estimateUpdateRequestFee(uint256 _evmGasPrice) - virtual override - public view - returns (uint) - { - return (IWitOracleLegacy(address(witOracle)).estimateBaseFee(_evmGasPrice, 32) - * (100 + __baseFeeOverheadPercentage) - ) / 100; - } - function lastValidQueryId(bytes4 feedId) override public view returns (Witnet.QueryId) { - return _lastValidQueryId(feedId); + return WitPriceFeedsDataLib.lastValidQueryId(witOracle, feedId); } function lastValidQueryResponse(bytes4 feedId) override public view returns (Witnet.QueryResponse memory) { - return witOracle.getQueryResponse(_lastValidQueryId(feedId)); + return witOracle.getQueryResponse( + WitPriceFeedsDataLib.lastValidQueryId(witOracle, feedId) + ); } function latestUpdateQueryId(bytes4 feedId) override public view returns (Witnet.QueryId) { - return __records_(feedId).latestUpdateQueryId; + return WitPriceFeedsDataLib.seekRecord(feedId).latestUpdateQueryId; } function latestUpdateQueryRequest(bytes4 feedId) @@ -271,7 +264,7 @@ contract WitPriceFeedsUpgradable override public view returns (Witnet.ResultStatus) { - return _coalesceQueryResultStatus(latestUpdateQueryId(feedId)); + return WitPriceFeedsDataLib.latestUpdateQueryResultStatus(witOracle, feedId); } function latestUpdateQueryResultStatusDescription(bytes4 feedId) @@ -287,7 +280,7 @@ contract WitPriceFeedsUpgradable override public view returns (bytes memory) { - Record storage __record = __records_(feedId); + WitPriceFeedsDataLib.Record storage __record = WitPriceFeedsDataLib.seekRecord(feedId); _require( __record.radHash != 0, "no RAD hash" @@ -299,7 +292,7 @@ contract WitPriceFeedsUpgradable override public view returns (bytes32) { - return __records_(feedId).radHash; + return WitPriceFeedsDataLib.seekRecord(feedId).radHash; } function lookupWitOracleRadonRetrievals(bytes4 feedId) @@ -322,12 +315,8 @@ contract WitPriceFeedsUpgradable function requestUpdate(bytes4 feedId, Witnet.QuerySLA calldata updateSLA) public payable virtual override - returns (uint256 _usedFunds) + returns (uint256) { - _require( - updateSLA.equalOrGreaterThan(__defaultRadonSLA), - "unsecure update" - ); return __requestUpdate(feedId, updateSLA); } @@ -428,36 +417,25 @@ contract WitPriceFeedsUpgradable Ownable.transferOwnership(_newOwner); } - function deleteFeed(string calldata caption) - virtual override - external - onlyOwner - { - bytes4 feedId = hash(caption); - bytes4[] storage __ids = __storage().ids; - Record storage __record = __records_(feedId); - uint _index = __record.index; - _require(_index != 0, "unknown feed"); - { - bytes4 _lastFeedId = __ids[__ids.length - 1]; - __ids[_index - 1] = _lastFeedId; - __ids.pop(); - __records_(_lastFeedId).index = _index; - delete __storage().records[feedId]; + function deleteFeed(string calldata caption) virtual override external onlyOwner { + try WitPriceFeedsDataLib.deleteFeed(caption) { + + } catch Error(string memory _reason) { + _revert(_reason); + + } catch (bytes memory) { + _revertWitPriceFeedsDataLibUnhandledException(); } - emit WitnetFeedDeleted(feedId); } - function deleteFeeds() - virtual override - external - onlyOwner - { - bytes4[] storage __ids = __storage().ids; - for (uint _ix = __ids.length; _ix > 0; _ix --) { - bytes4 _feedId = __ids[_ix - 1]; - delete __storage().records[_feedId]; __ids.pop(); - emit WitnetFeedDeleted(_feedId); + function deleteFeeds() virtual override external onlyOwner { + try WitPriceFeedsDataLib.deleteFeeds() { + + } catch Error(string memory _reason) { + _revert(_reason); + + } catch (bytes memory) { + _revertWitPriceFeedsDataLibUnhandledException(); } } @@ -475,7 +453,6 @@ contract WitPriceFeedsUpgradable { _require(defaultSLA.isValid(), "invalid SLA"); __defaultRadonSLA = defaultSLA; - emit WitnetRadonSLA(defaultSLA); } function settleFeedRequest(string calldata caption, bytes32 radHash) @@ -486,21 +463,14 @@ contract WitPriceFeedsUpgradable _registry().lookupRadonRequestResultDataType(radHash) == dataType, "bad result data type" ); - bytes4 feedId = hash(caption); - Record storage __record = __records_(feedId); - if (__record.index == 0) { - // settle new feed: - __record.caption = caption; - __record.decimals = _validateCaption(caption); - __record.index = __storage().ids.length + 1; - __record.radHash = radHash; - __storage().ids.push(feedId); - } else if (__record.radHash != radHash) { - // update radHash on existing feed: - __record.radHash = radHash; - __record.solver = address(0); + try WitPriceFeedsDataLib.settleFeedRequest(caption, radHash) { + + } catch Error(string memory _reason) { + _revert(_reason); + + } catch (bytes memory) { + _revertWitPriceFeedsDataLibUnhandledException(); } - emit WitnetFeedSettled(feedId, radHash); } function settleFeedRequest(string calldata caption, WitOracleRequest request) @@ -529,60 +499,22 @@ contract WitPriceFeedsUpgradable override external onlyOwner { + _require( + bytes6(bytes(caption)) == bytes6(__prefix), + "bad caption prefix" + ); _require( solver != address(0), "no solver address" ); - bytes4 feedId = hash(caption); - Record storage __record = __records_(feedId); - if (__record.index == 0) { - // settle new feed: - __record.caption = caption; - __record.decimals = _validateCaption(caption); - __record.index = __storage().ids.length + 1; - __record.solver = solver; - __storage().ids.push(feedId); - } else if (__record.solver != solver) { - // update radHash on existing feed: - __record.radHash = 0; - __record.solver = solver; - } - // validate solver first-level dependencies - { - // solhint-disable-next-line avoid-low-level-calls - (bool _success, bytes memory _reason) = solver.delegatecall(abi.encodeWithSelector( - IWitPriceFeedsSolver.validate.selector, - feedId, - deps - )); - if (!_success) { - assembly { - _reason := add(_reason, 4) - } - _revert(string(abi.encodePacked( - "solver validation failed: ", - string(abi.decode(_reason,(string))) - ))); - } - } - // smoke-test the solver - { - // solhint-disable-next-line avoid-low-level-calls - (bool _success, bytes memory _reason) = address(this).staticcall(abi.encodeWithSelector( - IWitPriceFeedsSolver.solve.selector, - feedId - )); - if (!_success) { - assembly { - _reason := add(_reason, 4) - } - _revert(string(abi.encodePacked( - "smoke-test failed: ", - string(abi.decode(_reason,(string))) - ))); - } + try WitPriceFeedsDataLib.settleFeedSolver(caption, solver, deps) { + + } catch Error(string memory _reason) { + _revert(_reason); + + } catch (bytes memory) { + _revertWitPriceFeedsDataLibUnhandledException(); } - emit WitnetFeedSolverSettled(feedId, solver); } @@ -594,7 +526,7 @@ contract WitPriceFeedsUpgradable external view returns (uint8) { - return __records_(feedId).decimals; + return WitPriceFeedsDataLib.seekRecord(feedId).decimals; } function lookupPriceSolver(bytes4 feedId) @@ -602,12 +534,7 @@ contract WitPriceFeedsUpgradable external view returns (IWitPriceFeedsSolver _solverAddress, string[] memory _solverDeps) { - _solverAddress = IWitPriceFeedsSolver(__records_(feedId).solver); - bytes4[] memory _deps = _depsOf(feedId); - _solverDeps = new string[](_deps.length); - for (uint _ix = 0; _ix < _deps.length; _ix ++) { - _solverDeps[_ix] = lookupCaption(_deps[_ix]); - } + return WitPriceFeedsDataLib.seekPriceSolver(feedId); } function latestPrice(bytes4 feedId) @@ -615,42 +542,17 @@ contract WitPriceFeedsUpgradable public view returns (IWitPriceFeedsSolver.Price memory) { - Witnet.QueryId _queryId = _lastValidQueryId(feedId); - if (Witnet.QueryId.unwrap(_queryId) > 0) { - Witnet.DataResult memory _lastValidResult = witOracle.getQueryResult(_queryId); - return IWitPriceFeedsSolver.Price({ - value: _lastValidResult.fetchUint(), - timestamp: _lastValidResult.timestamp, - drTxHash: _lastValidResult.drTxHash, - status: latestUpdateQueryResultStatus(feedId) - }); - } else { - address _solver = __records_(feedId).solver; - if (_solver != address(0)) { - // solhint-disable-next-line avoid-low-level-calls - (bool _success, bytes memory _result) = address(this).staticcall(abi.encodeWithSelector( - IWitPriceFeedsSolver.solve.selector, - feedId - )); - if (!_success) { - assembly { - _result := add(_result, 4) - } - revert(string(abi.encodePacked( - "WitPriceFeeds: ", - string(abi.decode(_result, (string))) - ))); - } else { - return abi.decode(_result, (IWitPriceFeedsSolver.Price)); - } - } else { - return IWitPriceFeedsSolver.Price({ - value: 0, - timestamp: Witnet.ResultTimestamp.wrap(0), - drTxHash: Witnet.TransactionHash.wrap(0), - status: latestUpdateQueryResultStatus(feedId) - }); - } + try WitPriceFeedsDataLib.latestPrice( + witOracle, + feedId + ) returns (IWitPriceFeedsSolver.Price memory _latestPrice) { + return _latestPrice; + + } catch Error(string memory _reason) { + _revert(_reason); + + } catch (bytes memory) { + _revertWitPriceFeedsDataLibUnhandledException(); } } @@ -670,25 +572,36 @@ contract WitPriceFeedsUpgradable // --- Implements 'IWitPriceFeedsSolverFactory' --------------------------------------------------------------------- function deployPriceSolver(bytes calldata initcode, bytes calldata constructorParams) - virtual override - external + virtual override external onlyOwner - returns (address _solver) + returns (address) { - _solver = WitPriceFeedsLib.deployPriceSolver(initcode, constructorParams); - emit NewPriceFeedsSolver( - _solver, - _solver.codehash, + try WitPriceFeedsDataLib.deployPriceSolver( + initcode, constructorParams - ); + ) returns ( + address _solver + ) { + emit NewPriceFeedsSolver( + _solver, + _solver.codehash, + constructorParams + ); + return _solver; + + } catch Error(string memory _reason) { + _revert(_reason); + + } catch (bytes memory) { + _revertWitPriceFeedsDataLibUnhandledException(); + } } function determinePriceSolverAddress(bytes calldata initcode, bytes calldata constructorParams) - virtual override - public view + virtual override public view returns (address _address) { - return WitPriceFeedsLib.determinePriceSolverAddress(initcode, constructorParams); + return WitPriceFeedsDataLib.determinePriceSolverAddress(initcode, constructorParams); } @@ -714,124 +627,110 @@ contract WitPriceFeedsUpgradable // ================================================================================================================ // --- Internal methods ------------------------------------------------------------------------------------------- - function _coalesceQueryResultStatus(Witnet.QueryId _queryId) - internal view - returns (Witnet.ResultStatus) - { - if (Witnet.QueryId.unwrap(_queryId) > 0) { - return witOracle.getQueryResultStatus(_queryId); - } else { - return Witnet.ResultStatus.NoErrors; - } - } - function _footprintOf(bytes4 _id4) virtual internal view returns (bytes4) { - if (__records_(_id4).radHash != bytes32(0)) { - return bytes4(keccak256(abi.encode(_id4, __records_(_id4).radHash))); + if (WitPriceFeedsDataLib.seekRecord(_id4).radHash != bytes32(0)) { + return bytes4(keccak256(abi.encode(_id4, WitPriceFeedsDataLib.seekRecord(_id4).radHash))); } else { - return bytes4(keccak256(abi.encode(_id4, __records_(_id4).solverDepsFlag))); - } - } - - function _lastValidQueryId(bytes4 feedId) - virtual internal view - returns (Witnet.QueryId _queryId) - { - _queryId = latestUpdateQueryId(feedId); - if ( - Witnet.QueryId.unwrap(_queryId) == 0 - || witOracle.getQueryResultStatus(_queryId) != Witnet.ResultStatus.NoErrors - ) { - _queryId = __records_(feedId).lastValidQueryId; + return bytes4(keccak256(abi.encode(_id4, WitPriceFeedsDataLib.seekRecord(_id4).solverDepsFlag))); } } function _validateCaption(string calldata caption) internal view returns (uint8) { - try WitPriceFeedsLib.validateCaption(__prefix, caption) returns (uint8 _decimals) { + _require( + bytes6(bytes(caption)) == bytes6(__prefix), + "bad caption prefix" + ); + try WitPriceFeedsDataLib.validateCaption(caption) returns (uint8 _decimals) { return _decimals; - } catch Error(string memory reason) { - _revert(reason); + + } catch Error(string memory _reason) { + _revert(_reason); + + } catch (bytes memory) { + _revertWitPriceFeedsDataLibUnhandledException(); } } + function _revertWitPriceFeedsDataLibUnhandledException() internal view { + _revert(_revertWitPriceFeedsDataLibUnhandledExceptionReason()); + } + + function _revertWitPriceFeedsDataLibUnhandledExceptionReason() internal pure returns (string memory) { + return string(abi.encodePacked( + type(WitPriceFeedsDataLib).name, + ": unhandled assertion" + )); + } + function __requestUpdate(bytes4[] memory _deps, Witnet.QuerySLA memory sla) virtual internal - returns (uint256 _usedFunds) + returns (uint256 _evmUsedFunds) { - uint _partial = msg.value / _deps.length; + uint _evmUnitaryUpdateRequestFee = msg.value / _deps.length; for (uint _ix = 0; _ix < _deps.length; _ix ++) { - _usedFunds += this.requestUpdate{value: _partial}(_deps[_ix], sla); + _evmUsedFunds += this.requestUpdate{ + value: _evmUnitaryUpdateRequestFee + }( + _deps[_ix], + sla + ); } } - function __requestUpdate(bytes4 feedId, Witnet.QuerySLA memory querySLA) + function __requestUpdate( + bytes4 feedId, + Witnet.QuerySLA memory querySLA + ) virtual internal - returns (uint256 _usedFunds) - { - // TODO: let requester settle the reward (see WRV2.randomize(..)) - Record storage __feed = __records_(feedId); - if (__feed.radHash != 0) { - _usedFunds = estimateUpdateRequestFee(tx.gasprice); - _require(msg.value >= _usedFunds, "insufficient reward"); - Witnet.QueryId _latestId = __feed.latestUpdateQueryId; - Witnet.ResultStatus _latestStatus = _coalesceQueryResultStatus(_latestId); - if (_latestStatus.keepWaiting()) { - // latest update is still pending, so just increase the reward - // accordingly to current tx gasprice: - uint72 _evmReward = Witnet.QueryReward.unwrap(witOracle.getQueryEvmReward(_latestId)); - int _deltaReward = int(int72(_evmReward)) - int(_usedFunds); - if (_deltaReward > 0) { - _usedFunds = uint(_deltaReward); - witOracle.upgradeQueryEvmReward{value: _usedFunds}(_latestId); - } else { - _usedFunds = 0; - } - } else { - // Check if latest update ended successfully: - if (_latestStatus == Witnet.ResultStatus.NoErrors) { - // If so, remove previous last valid query from the WRB: - if (Witnet.QueryId.unwrap(__feed.lastValidQueryId) > 0) { - _usedFunds += Witnet.QueryReward.unwrap( - witOracle.deleteQuery(__feed.lastValidQueryId) - ); - } - __feed.lastValidQueryId = _latestId; - } else { - // Otherwise, try to delete latest query, as it was faulty - // and we are about to post a new update request: - try witOracle.deleteQuery(_latestId) - returns (Witnet.QueryReward _unsedReward) { - _usedFunds += Witnet.QueryReward.unwrap(_unsedReward); - } catch {} + returns (uint256) + { + if (WitPriceFeedsDataLib.seekRecord(feedId).radHash != 0) { + // TODO: let requester settle the reward (see WRV2.randomize(..)) + uint256 _evmUpdateRequestFee = estimateUpdateRequestFee(tx.gasprice); + _require( + msg.value >= _evmUpdateRequestFee, + "insufficient update fee" + ); + try WitPriceFeedsDataLib.requestUpdate( + witOracle, + feedId, + querySLA, + _evmUpdateRequestFee + + ) returns ( + Witnet.QueryId _latestQueryId, + uint256 _evmUsedFunds + + ) { + if (_evmUsedFunds < msg.value) { + // transfer back unused funds: + payable(msg.sender).transfer(msg.value - _evmUsedFunds); } - // Post update request to the WRB: - _latestId = witOracle.postQuery{value: _usedFunds}( - __feed.radHash, - querySLA - ); - // Update latest query id: - __feed.latestUpdateQueryId = _latestId; // solhint-disable avoid-tx-origin: - emit PullingUpdate( - tx.origin, - _msgSender(), - feedId, - Witnet.QueryId.unwrap(_latestId) - ); - } - } else if (__feed.solver != address(0)) { - _usedFunds = __requestUpdate( - _depsOf(feedId), + emit PullingUpdate(tx.origin, _msgSender(), feedId, _latestQueryId); + return _evmUsedFunds; + + } catch Error(string memory _reason) { + _revert(_reason); + + } catch (bytes memory) { + _revertWitPriceFeedsDataLibUnhandledException(); + } + + } else if (WitPriceFeedsDataLib.seekRecord(feedId).solver != address(0)) { + return __requestUpdate( + WitPriceFeedsDataLib.depsOf(feedId), querySLA ); + } else { _revert("unknown feed"); } - if (_usedFunds < msg.value) { - // transfer back unused funds: - payable(msg.sender).transfer(msg.value - _usedFunds); - } + } + + function __storage() internal pure returns (WitPriceFeedsDataLib.Storage storage) { + return WitPriceFeedsDataLib.data(); } } diff --git a/contracts/data/WitPriceFeedsData.sol b/contracts/data/WitPriceFeedsData.sol deleted file mode 100644 index 2d8587be..00000000 --- a/contracts/data/WitPriceFeedsData.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.8.0 <0.9.0; - -import "../libs/Witnet.sol"; - -/// @title WitFeeds data model. -/// @author The Witnet Foundation. -abstract contract WitPriceFeedsData { - - bytes32 private constant _WIT_FEEDS_DATA_SLOTHASH = - /* keccak256("io.witnet.feeds.data") */ - 0xe36ea87c48340f2c23c9e1c9f72f5c5165184e75683a4d2a19148e5964c1d1ff; - - struct Storage { - bytes32 reserved; - bytes4[] ids; - mapping (bytes4 => Record) records; - } - - struct Record { - string caption; - uint8 decimals; - uint256 index; - Witnet.QueryId lastValidQueryId; - Witnet.QueryId latestUpdateQueryId; - bytes32 radHash; - address solver; // logic contract address for reducing values on routed feeds. - int256 solverReductor; // as to reduce resulting number of decimals on routed feeds. - bytes32 solverDepsFlag; // as to store ids of up to 8 depending feeds. - } - - // ================================================================================================ - // --- Internal functions ------------------------------------------------------------------------- - - /// @notice Returns storage pointer to where Storage data is located. - function __storage() - internal pure - returns (Storage storage _ptr) - { - assembly { - _ptr.slot := _WIT_FEEDS_DATA_SLOTHASH - } - } - - /// @notice Returns storage pointer to where Record for given feedId is located. - function __records_(bytes4 feedId) internal view returns (Record storage) { - return __storage().records[feedId]; - } - - /// @notice Returns array of feed ids from which given feed's value depends. - /// @dev Returns empty array on either unsupported or not-routed feeds. - /// @dev The maximum number of dependencies is hard-limited to 8, as to limit number - /// @dev of SSTORE operations (`__storage().records[feedId].solverDepsFlag`), - /// @dev no matter the actual number of depending feeds involved. - function _depsOf(bytes4 feedId) internal view returns (bytes4[] memory _deps) { - bytes32 _solverDepsFlag = __storage().records[feedId].solverDepsFlag; - _deps = new bytes4[](8); - uint _len; - for (_len = 0; _len < 8; _len ++) { - _deps[_len] = bytes4(_solverDepsFlag); - if (_deps[_len] == 0) { - break; - } else { - _solverDepsFlag <<= 32; - } - } - assembly { - // reset length to actual number of dependencies: - mstore(_deps, _len) - } - } -} \ No newline at end of file diff --git a/contracts/data/WitPriceFeedsDataLib.sol b/contracts/data/WitPriceFeedsDataLib.sol new file mode 100644 index 00000000..852006ad --- /dev/null +++ b/contracts/data/WitPriceFeedsDataLib.sol @@ -0,0 +1,442 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0 <0.9.0; + +import "../interfaces/IWitFeedsAdmin.sol"; +import "../interfaces/IWitPriceFeedsSolver.sol"; + +import "../libs/Slices.sol"; +import "../libs/Witnet.sol"; + +/// @title WitFeeds data model. +/// @author The Witnet Foundation. +library WitPriceFeedsDataLib { + + using Slices for string; + using Slices for Slices.Slice; + + using Witnet for Witnet.DataResult; + using Witnet for Witnet.QueryId; + using Witnet for Witnet.ResultStatus; + + bytes32 private constant _WIT_FEEDS_DATA_SLOTHASH = + /* keccak256("io.witnet.feeds.data") */ + 0xe36ea87c48340f2c23c9e1c9f72f5c5165184e75683a4d2a19148e5964c1d1ff; + + struct Storage { + bytes32 reserved; + bytes4[] ids; + mapping (bytes4 => Record) records; + } + + struct Record { + string caption; + uint8 decimals; + uint256 index; + Witnet.QueryId lastValidQueryId; + Witnet.QueryId latestUpdateQueryId; + bytes32 radHash; + address solver; // logic contract address for reducing values on routed feeds. + int256 solverReductor; // as to reduce resulting number of decimals on routed feeds. + bytes32 solverDepsFlag; // as to store ids of up to 8 depending feeds. + } + + + // ================================================================================================ + // --- Internal functions ------------------------------------------------------------------------- + + /// @notice Returns array of feed ids from which given feed's value depends. + /// @dev Returns empty array on either unsupported or not-routed feeds. + /// @dev The maximum number of dependencies is hard-limited to 8, as to limit number + /// @dev of SSTORE operations (`__storage().records[feedId].solverDepsFlag`), + /// @dev no matter the actual number of depending feeds involved. + function depsOf(bytes4 feedId) public view returns (bytes4[] memory _deps) { + bytes32 _solverDepsFlag = data().records[feedId].solverDepsFlag; + _deps = new bytes4[](8); + uint _len; + for (_len = 0; _len < 8; _len ++) { + _deps[_len] = bytes4(_solverDepsFlag); + if (_deps[_len] == 0) { + break; + } else { + _solverDepsFlag <<= 32; + } + } + assembly { + // reset length to actual number of dependencies: + mstore(_deps, _len) + } + } + + /// @notice Returns storage pointer to where Storage data is located. + function data() + internal pure + returns (Storage storage _ptr) + { + assembly { + _ptr.slot := _WIT_FEEDS_DATA_SLOTHASH + } + } + + function hash(string memory caption) internal pure returns (bytes4) { + return bytes4(keccak256(bytes(caption))); + } + + function lastValidQueryId(WitOracle witOracle, bytes4 feedId) + internal view + returns (Witnet.QueryId _queryId) + { + _queryId = seekRecord(feedId).latestUpdateQueryId; + if ( + _queryId.isZero() + || witOracle.getQueryResultStatus(_queryId) != Witnet.ResultStatus.NoErrors + ) { + _queryId = seekRecord(feedId).lastValidQueryId; + } + } + + function latestUpdateQueryResultStatus(WitOracle witOracle, bytes4 feedId) + internal view + returns (Witnet.ResultStatus) + { + Witnet.QueryId _queryId = seekRecord(feedId).latestUpdateQueryId; + if (!_queryId.isZero()) { + return witOracle.getQueryResultStatus(_queryId); + } else { + return Witnet.ResultStatus.NoErrors; + } + } + + /// @notice Returns storage pointer to where Record for given feedId is located. + function seekRecord(bytes4 feedId) internal view returns (Record storage) { + return data().records[feedId]; + } + + function seekPriceSolver(bytes4 feedId) internal view returns ( + IWitPriceFeedsSolver _solverAddress, + string[] memory _solverDeps + ) + { + _solverAddress = IWitPriceFeedsSolver(seekRecord(feedId).solver); + bytes4[] memory _deps = depsOf(feedId); + _solverDeps = new string[](_deps.length); + for (uint _ix = 0; _ix < _deps.length; _ix ++) { + _solverDeps[_ix] = seekRecord(_deps[_ix]).caption; + } + } + + + // ================================================================================================ + // --- Public functions --------------------------------------------------------------------------- + + function deleteFeed(string calldata caption) public { + bytes4 feedId = hash(caption); + bytes4[] storage __ids = data().ids; + Record storage __record = seekRecord(feedId); + uint _index = __record.index; + require(_index != 0, "unknown feed"); + bytes4 _lastFeedId = __ids[__ids.length - 1]; + __ids[_index - 1] = _lastFeedId; + __ids.pop(); + seekRecord(_lastFeedId).index = _index; + delete data().records[feedId]; + emit IWitFeedsAdmin.WitFeedDeleted(caption, feedId); + } + + function deleteFeeds() public { + bytes4[] storage __ids = data().ids; + for (uint _ix = __ids.length; _ix > 0; _ix --) { + bytes4 _feedId = __ids[_ix - 1]; + string memory _caption = data().records[_feedId].caption; + delete data().records[_feedId]; __ids.pop(); + emit IWitFeedsAdmin.WitFeedDeleted(_caption, _feedId); + } + } + + function latestPrice( + WitOracle witOracle, + bytes4 feedId + ) + public view + returns (IWitPriceFeedsSolver.Price memory) + { + Witnet.QueryId _queryId = lastValidQueryId(witOracle, feedId); + if (!_queryId.isZero()) { + Witnet.DataResult memory _lastValidResult = witOracle.getQueryResult(_queryId); + return IWitPriceFeedsSolver.Price({ + value: _lastValidResult.fetchUint(), + timestamp: _lastValidResult.timestamp, + drTxHash: _lastValidResult.drTxHash, + status: latestUpdateQueryResultStatus(witOracle, feedId) + }); + + } else { + address _solver = seekRecord(feedId).solver; + if (_solver != address(0)) { + // solhint-disable-next-line avoid-low-level-calls + (bool _success, bytes memory _result) = address(this).staticcall(abi.encodeWithSelector( + IWitPriceFeedsSolver.solve.selector, + feedId + )); + if (!_success) { + assembly { + _result := add(_result, 4) + } + revert(string(abi.decode(_result, (string)))); + } else { + return abi.decode(_result, (IWitPriceFeedsSolver.Price)); + } + } else { + return IWitPriceFeedsSolver.Price({ + value: 0, + timestamp: Witnet.ResultTimestamp.wrap(0), + drTxHash: Witnet.TransactionHash.wrap(0), + status: latestUpdateQueryResultStatus(witOracle, feedId) + }); + } + } + } + + function requestUpdate( + WitOracle witOracle, + bytes4 feedId, + Witnet.QuerySLA memory querySLA, + uint256 evmUpdateRequestFee + ) + public + returns ( + Witnet.QueryId _latestQueryId, + uint256 _evmUsedFunds + ) + { + Record storage __feed = seekRecord(feedId); + _latestQueryId = __feed.latestUpdateQueryId; + + Witnet.ResultStatus _latestStatus = latestUpdateQueryResultStatus(witOracle, feedId); + if (_latestStatus.keepWaiting()) { + uint72 _evmUpdateRequestCurrentFee = Witnet.QueryReward.unwrap( + witOracle.getQueryEvmReward(_latestQueryId) + ); + if (evmUpdateRequestFee > _evmUpdateRequestCurrentFee) { + // latest update is still pending, so just increase the reward + // accordingly to current tx gasprice: + _evmUsedFunds = (evmUpdateRequestFee - _evmUpdateRequestCurrentFee); + witOracle.upgradeQueryEvmReward{ + value: _evmUsedFunds + }( + _latestQueryId + ); + + } else { + _evmUsedFunds = 0; + } + + } else { + // Check if latest update ended successfully: + if (_latestStatus == Witnet.ResultStatus.NoErrors) { + // If so, remove previous last valid query from the WRB: + if (!__feed.lastValidQueryId.isZero()) { + evmUpdateRequestFee += Witnet.QueryReward.unwrap( + witOracle.deleteQuery(__feed.lastValidQueryId) + ); + } + __feed.lastValidQueryId = _latestQueryId; + } else { + // Otherwise, try to delete latest query, as it was faulty + // and we are about to post a new update request: + try witOracle.deleteQuery(_latestQueryId) returns (Witnet.QueryReward _unsedReward) { + evmUpdateRequestFee += Witnet.QueryReward.unwrap(_unsedReward); + + } catch {} + } + // Post update request to the WRB: + _evmUsedFunds = evmUpdateRequestFee; + _latestQueryId = witOracle.postQuery{ + value: _evmUsedFunds + }( + __feed.radHash, + querySLA + ); + // Update latest query id: + __feed.latestUpdateQueryId = _latestQueryId; + } + } + + function settleFeedRequest( + string calldata caption, + bytes32 radHash + ) + public + { + bytes4 feedId = hash(caption); + Record storage __record = seekRecord(feedId); + if (__record.index == 0) { + // settle new feed: + __record.caption = caption; + __record.decimals = validateCaption(caption); + __record.index = data().ids.length + 1; + __record.radHash = radHash; + data().ids.push(feedId); + } else if (__record.radHash != radHash) { + // update radHash on existing feed: + __record.radHash = radHash; + __record.solver = address(0); + } + emit IWitFeedsAdmin.WitFeedSettled(caption, feedId, radHash); + } + + function settleFeedSolver( + string calldata caption, + address solver, + string[] calldata deps + ) + public + { + bytes4 feedId = hash(caption); + Record storage __record = seekRecord(feedId); + if (__record.index == 0) { + // settle new feed: + __record.caption = caption; + __record.decimals = validateCaption(caption); + __record.index = data().ids.length + 1; + __record.solver = solver; + data().ids.push(feedId); + + } else if (__record.solver != solver) { + // update radHash on existing feed: + __record.radHash = 0; + __record.solver = solver; + } + // validate solver first-level dependencies + { + // solhint-disable-next-line avoid-low-level-calls + (bool _success, bytes memory _reason) = solver.delegatecall(abi.encodeWithSelector( + IWitPriceFeedsSolver.validate.selector, + feedId, + deps + )); + if (!_success) { + assembly { + _reason := add(_reason, 4) + } + revert(string(abi.encodePacked( + "solver validation failed: ", + string(abi.decode(_reason,(string))) + ))); + } + } + // smoke-test the solver + { + // solhint-disable-next-line avoid-low-level-calls + (bool _success, bytes memory _reason) = address(this).staticcall(abi.encodeWithSelector( + IWitPriceFeedsSolver.solve.selector, + feedId + )); + if (!_success) { + assembly { + _reason := add(_reason, 4) + } + revert(string(abi.encodePacked( + "smoke-test failed: ", + string(abi.decode(_reason,(string))) + ))); + } + } + emit IWitFeedsAdmin.WitFeedSolverSettled(caption, feedId, solver); + } + + function supportedFeeds() public view returns ( + bytes4[] memory _ids, + string[] memory _captions, + bytes32[] memory _solvers + ) + { + _ids = data().ids; + _captions = new string[](_ids.length); + _solvers = new bytes32[](_ids.length); + for (uint _ix = 0; _ix < _ids.length; _ix ++) { + Record storage __record = seekRecord(_ids[_ix]); + _captions[_ix] = __record.caption; + _solvers[_ix] = ( + address(__record.solver) == address(0) + ? __record.radHash + : bytes32(bytes20(__record.solver)) + ); + } + } + + // --- IWitPriceFeedsSolver public functions ------------------------------------------------------------- + + function deployPriceSolver( + bytes calldata initcode, + bytes calldata constructorParams + ) + external + returns (address _solver) + { + _solver = determinePriceSolverAddress(initcode, constructorParams); + if (_solver.code.length == 0) { + bytes memory _bytecode = _completeInitCode(initcode, constructorParams); + address _createdContract; + assembly { + _createdContract := create2( + 0, + add(_bytecode, 0x20), + mload(_bytecode), + 0 + ) + } + // assert(_solver == _createdContract); // fails on TEN chains + _solver = _createdContract; + require( + IWitPriceFeedsSolver(_solver).specs() == type(IWitPriceFeedsSolver).interfaceId, + "uncompliant solver implementation" + ); + } + } + + function determinePriceSolverAddress( + bytes calldata initcode, + bytes calldata constructorParams + ) + public view + returns (address) + { + return address( + uint160(uint(keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + bytes32(0), + keccak256(_completeInitCode(initcode, constructorParams)) + ) + ))) + ); + } + + function validateCaption(string calldata caption) public pure returns (uint8) { + Slices.Slice memory _caption = caption.toSlice(); + Slices.Slice memory _delim = string("-").toSlice(); + string[] memory _parts = new string[](_caption.count(_delim) + 1); + for (uint _ix = 0; _ix < _parts.length; _ix ++) { + _parts[_ix] = _caption.split(_delim).toString(); + } + (uint _decimals, bool _success) = Witnet.tryUint(_parts[_parts.length - 1]); + require(_success, "bad decimals"); + return uint8(_decimals); + } + + + // ================================================================================================ + // --- Private functions -------------------------------------------------------------------------- + + function _completeInitCode(bytes calldata initcode, bytes calldata constructorParams) + private pure + returns (bytes memory) + { + return abi.encodePacked( + initcode, + constructorParams + ); + } +} diff --git a/contracts/interfaces/IWitFeedsAdmin.sol b/contracts/interfaces/IWitFeedsAdmin.sol index 3b292834..4867f63d 100644 --- a/contracts/interfaces/IWitFeedsAdmin.sol +++ b/contracts/interfaces/IWitFeedsAdmin.sol @@ -9,10 +9,9 @@ import "../WitOracleRequestTemplate.sol"; interface IWitFeedsAdmin { - event WitnetFeedDeleted(bytes4 feedId); - event WitnetFeedSettled(bytes4 feedId, bytes32 radHash); - event WitnetFeedSolverSettled(bytes4 feedId, address solver); - event WitnetRadonSLA(Witnet.QuerySLA sla); + event WitFeedDeleted(string caption, bytes4 feedId); + event WitFeedSettled(string caption, bytes4 feedId, bytes32 radHash); + event WitFeedSolverSettled(string caption, bytes4 feedId, address solver); function acceptOwnership() external; function baseFeeOverheadPercentage() external view returns (uint16); diff --git a/contracts/interfaces/IWitFeedsEvents.sol b/contracts/interfaces/IWitFeedsEvents.sol index ee98537c..58d82df9 100644 --- a/contracts/interfaces/IWitFeedsEvents.sol +++ b/contracts/interfaces/IWitFeedsEvents.sol @@ -2,6 +2,8 @@ pragma solidity >=0.8.0 <0.9.0; +import "../libs/Witnet.sol"; + interface IWitFeedsEvents { /// A fresh update on the data feed identified as `erc2364Id4` has just been @@ -11,6 +13,6 @@ interface IWitFeedsEvents { address evmOrigin, address evmSender, bytes4 erc2362Id4, - uint256 witOracleQueryId + Witnet.QueryId witOracleQueryId ); } diff --git a/contracts/libs/WitPriceFeedsLib.sol b/contracts/libs/WitPriceFeedsLib.sol deleted file mode 100644 index 59878429..00000000 --- a/contracts/libs/WitPriceFeedsLib.sol +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.8.0 <0.9.0; - -import "../interfaces/IWitPriceFeedsSolver.sol"; -import "../interfaces/IWitPriceFeedsSolverFactory.sol"; - -import "../libs/Slices.sol"; - -/// @title Ancillary deployable library for WitPriceFeeds. -/// @dev Features: -/// @dev - deployment of counter-factual IWitPriceFeedsSolver instances. -/// @dev - validation of feed caption strings. -/// @author The Witnet Foundation. -library WitPriceFeedsLib { - - using Slices for string; - using Slices for Slices.Slice; - - function deployPriceSolver( - bytes calldata initcode, - bytes calldata constructorParams - ) - external - returns (address _solver) - { - _solver = determinePriceSolverAddress(initcode, constructorParams); - if (_solver.code.length == 0) { - bytes memory _bytecode = _completeInitCode(initcode, constructorParams); - address _createdContract; - assembly { - _createdContract := create2( - 0, - add(_bytecode, 0x20), - mload(_bytecode), - 0 - ) - } - // assert(_solver == _createdContract); // fails on TEN chains - _solver = _createdContract; - require( - IWitPriceFeedsSolver(_solver).specs() == type(IWitPriceFeedsSolver).interfaceId, - "WitPriceFeedsLib: uncompliant solver implementation" - ); - } - } - - function determinePriceSolverAddress( - bytes calldata initcode, - bytes calldata constructorParams - ) - public view - returns (address) - { - return address( - uint160(uint(keccak256( - abi.encodePacked( - bytes1(0xff), - address(this), - bytes32(0), - keccak256(_completeInitCode(initcode, constructorParams)) - ) - ))) - ); - } - - function validateCaption(bytes32 prefix, string calldata caption) - external pure - returns (uint8) - { - require( - bytes6(bytes(caption)) == bytes6(prefix), - "WitPriceFeedsLib: bad caption prefix" - ); - Slices.Slice memory _caption = caption.toSlice(); - Slices.Slice memory _delim = string("-").toSlice(); - string[] memory _parts = new string[](_caption.count(_delim) + 1); - for (uint _ix = 0; _ix < _parts.length; _ix ++) { - _parts[_ix] = _caption.split(_delim).toString(); - } - (uint _decimals, bool _success) = Witnet.tryUint(_parts[_parts.length - 1]); - require(_success, "WitPriceFeedsLib: bad decimals"); - return uint8(_decimals); - } - - function _completeInitCode(bytes calldata initcode, bytes calldata constructorParams) - private pure - returns (bytes memory) - { - return abi.encodePacked( - initcode, - constructorParams - ); - } - -} diff --git a/contracts/mockups/WitPriceFeedsSolverBase.sol b/contracts/mockups/WitPriceFeedsSolverBase.sol index 0ce11048..26f62330 100644 --- a/contracts/mockups/WitPriceFeedsSolverBase.sol +++ b/contracts/mockups/WitPriceFeedsSolverBase.sol @@ -2,13 +2,12 @@ pragma solidity >=0.8.0 <0.9.0; -import "../data/WitPriceFeedsData.sol"; +import "../data/WitPriceFeedsDataLib.sol"; import "../interfaces/IWitPriceFeeds.sol"; abstract contract WitPriceFeedsSolverBase is - IWitPriceFeedsSolver, - WitPriceFeedsData + IWitPriceFeedsSolver { address public immutable override delegator; @@ -37,7 +36,7 @@ abstract contract WitPriceFeedsSolverBase ); for (uint _ix = 0; _ix < deps.length; _ix ++) { bytes4 _depsId4 = bytes4(keccak256(bytes(deps[_ix]))); - Record storage __depsFeed = __records_(_depsId4); + WitPriceFeedsDataLib.Record storage __depsFeed = WitPriceFeedsDataLib.seekRecord(_depsId4); require( __depsFeed.index > 0, string(abi.encodePacked( @@ -55,7 +54,7 @@ abstract contract WitPriceFeedsSolverBase _depsFlag |= (bytes32(_depsId4) >> (32 * _ix)); _innerDecimals += __depsFeed.decimals; } - Record storage __feed = __records_(feedId); + WitPriceFeedsDataLib.Record storage __feed = WitPriceFeedsDataLib.seekRecord(feedId); __feed.solverReductor = int(uint(__feed.decimals)) - int(_innerDecimals); __feed.solverDepsFlag = _depsFlag; } diff --git a/migrations/scripts/3_framework.js b/migrations/scripts/3_framework.js index f394f5da..22da5507 100644 --- a/migrations/scripts/3_framework.js +++ b/migrations/scripts/3_framework.js @@ -408,7 +408,7 @@ async function deployTarget (network, target, targetSpecs, networkArtifacts, leg try { utils.traceTx(await deployer.deploy(targetInitCode, targetSalt, { from: targetSpecs.from })) } catch (ex) { - panic("Deployment failed", `Expected address: ${targetAddr}`, ex) + panic("Deployment failed", `Expected address: ${targetAddr}`) } if (!constructorArgs[network]) constructorArgs[network] = {} constructorArgs[network][target] = targetConstructorArgs diff --git a/scripts/verify-libs.js b/scripts/verify-libs.js index 06829182..93bf24c5 100644 --- a/scripts/verify-libs.js +++ b/scripts/verify-libs.js @@ -22,7 +22,7 @@ const libs = [ artifacts.WitOracleRadonEncodingLib, artifacts.WitOracleResultErrorsLib, artifacts.WitOracleDataLib, - artifacts.WitPriceFeedsLib, + artifacts.WitPriceFeedsDataLib, ] for (const index in libs) { utils.traceVerify(network, `${libs[index]}@${addresses[network][libs[index]]}`) diff --git a/settings/artifacts.js b/settings/artifacts.js index a21184ea..4998fdf8 100644 --- a/settings/artifacts.js +++ b/settings/artifacts.js @@ -14,7 +14,7 @@ module.exports = { WitOracleDataLib: "WitOracleDataLib", WitOracleRadonEncodingLib: "WitOracleRadonEncodingLib", WitOracleResultStatusLib: "WitOracleResultStatusLib", - WitPriceFeedsLib: "WitPriceFeedsLib", + WitPriceFeedsDataLib: "WitPriceFeedsDataLib", }, }, "polygon:amoy": { diff --git a/settings/specs.js b/settings/specs.js index d44bb97a..7589a2de 100644 --- a/settings/specs.js +++ b/settings/specs.js @@ -44,7 +44,7 @@ module.exports = { }, WitPriceFeeds: { baseLibs: [ - "WitPriceFeedsLib", + "WitPriceFeedsDataLib", ], from: "0xF121b71715E71DDeD592F1125a06D4ED06F0694D", vanity: 1865150170, // 0x1111AbA2164AcdC6D291b08DfB374280035E1111