From 78c0d0e96cd4d27645b0e73609b33e33686a100a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Wed, 27 Dec 2023 14:48:15 +0100 Subject: [PATCH 1/7] refactor: merge together WitnetRequestBoardData, WitnetRequestBoardDataACLs --- .../WitnetRequestBoardTrustableBase.sol | 16 ++++---- contracts/data/WitnetRequestBoardData.sol | 24 ++++++++++- contracts/data/WitnetRequestBoardDataACLs.sol | 41 ------------------- 3 files changed, 31 insertions(+), 50 deletions(-) delete mode 100644 contracts/data/WitnetRequestBoardDataACLs.sol diff --git a/contracts/core/defaults/WitnetRequestBoardTrustableBase.sol b/contracts/core/defaults/WitnetRequestBoardTrustableBase.sol index f8056eb5..baf4096e 100644 --- a/contracts/core/defaults/WitnetRequestBoardTrustableBase.sol +++ b/contracts/core/defaults/WitnetRequestBoardTrustableBase.sol @@ -7,7 +7,7 @@ import "../WitnetUpgradableBase.sol"; import "../../WitnetRequestBoard.sol"; import "../../WitnetRequestFactory.sol"; -import "../../data/WitnetRequestBoardDataACLs.sol"; +import "../../data/WitnetRequestBoardData.sol"; import "../../interfaces/IWitnetRequest.sol"; import "../../interfaces/IWitnetRequestBoardAdminACLs.sol"; import "../../interfaces/V2/IWitnetRequestBoardReporter.sol"; @@ -24,7 +24,7 @@ abstract contract WitnetRequestBoardTrustableBase is WitnetUpgradableBase, WitnetRequestBoard, - WitnetRequestBoardDataACLs, + WitnetRequestBoardData, IWitnetRequestBoardReporter, IWitnetRequestBoardAdminACLs, Payable @@ -206,7 +206,7 @@ abstract contract WitnetRequestBoardTrustableBase uint16 _resultMaxSize = registry.lookupRadonRequestResultMaxSize(radHash); require( _resultMaxSize > 0, - "WitnetRequestBoardTrustableDefault: invalid RAD" + "WitnetRequestBoardTrustableBase: invalid RAD" ); return estimateBaseFee( gasPrice, @@ -570,13 +570,13 @@ abstract contract WitnetRequestBoardTrustableBase { require( _witnetResultTallyHash != 0, - "WitnetRequestBoardTrustableDefault: tally has cannot be zero" + "WitnetRequestBoardTrustableBase: tally hash cannot be zero" ); // Ensures the result bytes do not have zero length // This would not be a valid encoding with CBOR and could trigger a reentrancy attack require( _witnetResultCborBytes.length != 0, - "WitnetRequestBoardTrustableDefault: result cannot be empty" + "WitnetRequestBoardTrustableBase: result cannot be empty" ); // Do actual report: // solhint-disable not-rely-on-time @@ -612,17 +612,17 @@ abstract contract WitnetRequestBoardTrustableBase { require( _witnetResultTimestamp <= block.timestamp, - "WitnetRequestBoardTrustableDefault: bad timestamp" + "WitnetRequestBoardTrustableBase: bad timestamp" ); require( _witnetResultTallyHash != 0, - "WitnetRequestBoardTrustableDefault: Witnet tallyHash cannot be zero" + "WitnetRequestBoardTrustableBase: Witnet tallyHash cannot be zero" ); // Ensures the result bytes do not have zero length (this would not be a valid CBOR encoding // and could trigger a reentrancy attack) require( _witnetResultCborBytes.length != 0, - "WitnetRequestBoardTrustableDefault: result cannot be empty" + "WitnetRequestBoardTrustableBase: result cannot be empty" ); // Do actual report and return reward transfered to the reproter: return __reportResultAndReward( diff --git a/contracts/data/WitnetRequestBoardData.sol b/contracts/data/WitnetRequestBoardData.sol index 2b4ab01b..e79f4bfc 100644 --- a/contracts/data/WitnetRequestBoardData.sol +++ b/contracts/data/WitnetRequestBoardData.sol @@ -10,10 +10,18 @@ abstract contract WitnetRequestBoardData { using WitnetV2 for WitnetV2.Request; + bytes32 internal constant _WITNET_ACLS_DATA_SLOTHASH = + /* keccak256("io.witnet.boards.data.acls") */ + 0xa6db7263983f337bae2c9fb315730227961d1c1153ae1e10a56b5791465dd6fd; + bytes32 internal constant _WITNET_REQUEST_BOARD_DATA_SLOTHASH = /* keccak256("io.witnet.boards.data") */ 0xf595240b351bc8f951c2f53b26f4e78c32cb62122cf76c19b7fdda7d4968e183; + struct WitnetBoardACLs { + mapping (address => bool) isReporter_; + } + struct WitnetBoardState { address base; address owner; @@ -37,14 +45,28 @@ abstract contract WitnetRequestBoardData { modifier onlyRequester(uint256 _queryId) { require( msg.sender == __seekQueryRequest(_queryId).unpackRequester(), - "WitnetRequestBoardBase: not the requester" + "WitnetRequestBoard: not the requester" ); _; } + modifier onlyReporters { + require( + __acls().isReporter_[msg.sender], + "WitnetRequestBoard: unauthorized reporter" + ); + _; + } + // ================================================================================================================ // --- Internal functions ----------------------------------------------------------------------------------------- + function __acls() internal pure returns (WitnetBoardACLs storage _struct) { + assembly { + _struct.slot := _WITNET_ACLS_DATA_SLOTHASH + } + } + /// Gets query storage by query id. function __seekQuery(uint256 _queryId) internal view returns (WitnetV2.Query storage) { return __storage().queries[_queryId]; diff --git a/contracts/data/WitnetRequestBoardDataACLs.sol b/contracts/data/WitnetRequestBoardDataACLs.sol deleted file mode 100644 index 42ca1921..00000000 --- a/contracts/data/WitnetRequestBoardDataACLs.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.0 <0.9.0; - -import "./WitnetRequestBoardData.sol"; - -/// @title Witnet Access Control Lists storage layout, for Witnet-trusted request boards. -/// @author The Witnet Foundation. -abstract contract WitnetRequestBoardDataACLs - is - WitnetRequestBoardData -{ - bytes32 internal constant _WITNET_BOARD_ACLS_SLOTHASH = - /* keccak256("io.witnet.boards.data.acls") */ - 0xa6db7263983f337bae2c9fb315730227961d1c1153ae1e10a56b5791465dd6fd; - - struct WitnetBoardACLs { - mapping (address => bool) isReporter_; - } - - constructor() { - __acls().isReporter_[msg.sender] = true; - } - - modifier onlyReporters { - require( - __acls().isReporter_[msg.sender], - "WitnetRequestBoard: unauthorized reporter" - ); - _; - } - - // ================================================================================================================ - // --- Internal functions ----------------------------------------------------------------------------------------- - - function __acls() internal pure returns (WitnetBoardACLs storage _struct) { - assembly { - _struct.slot := _WITNET_BOARD_ACLS_SLOTHASH - } - } -} From fa4994eb1382b81afcc6d64c58e8a1d41bb65fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Wed, 27 Dec 2023 14:49:45 +0100 Subject: [PATCH 2/7] feat: improve WRB.estimateQueryEarnings --- .../WitnetRequestBoardTrustableBase.sol | 26 ++++------------ .../WitnetRequestBoardTrustableDefault.sol | 31 +++++++++++++++++++ .../interfaces/V2/IWitnetRequestBoard.sol | 1 - 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/contracts/core/defaults/WitnetRequestBoardTrustableBase.sol b/contracts/core/defaults/WitnetRequestBoardTrustableBase.sol index baf4096e..3de64005 100644 --- a/contracts/core/defaults/WitnetRequestBoardTrustableBase.sol +++ b/contracts/core/defaults/WitnetRequestBoardTrustableBase.sol @@ -122,6 +122,12 @@ abstract contract WitnetRequestBoardTrustableBase /// @param _callbackGasLimit Maximum gas to be spent when reporting the data request result. function estimateBaseFeeWithCallback(uint256 _gasPrice, uint96 _callbackGasLimit) virtual public view returns (uint256); + /// @notice Estimates the actual earnings (or loss), in WEI, that a reporter would get by reporting result to given query, + /// @notice based on the gas price of the calling transaction. Data requesters should consider upgrading the reward on + /// @notice queries providing no actual earnings. + /// @dev Fails if the query does not exist, or if deleted. + function estimateQueryEarnings(uint256[] calldata _witnetQueryIds, uint256 _gasPrice) virtual external view returns (int256); + // ================================================================================================================ // --- Overrides 'Upgradeable' ------------------------------------------------------------------------------------ @@ -528,26 +534,6 @@ abstract contract WitnetRequestBoardTrustableBase // ================================================================================================================ // --- Full implementation of IWitnetRequestBoardReporter --------------------------------------------------------- - /// @notice Estimates the actual earnings (or loss), in WEI, that a reporter would get by reporting result to given query, - /// @notice based on the gas price of the calling transaction. Data requesters should consider upgrading the reward on - /// @notice queries providing no actual earnings. - /// @dev Fails if the query does not exist, or if deleted. - function estimateQueryEarnings(uint256[] calldata _witnetQueryIds, uint256 _gasPrice) - virtual override - external view - returns (int256 _earnings) - { - uint256 _expenses; uint256 _revenues; - for (uint _ix = 0; _ix < _witnetQueryIds.length; _ix ++) { - if (_statusOf(_witnetQueryIds[_ix]) == WitnetV2.QueryStatus.Posted) { - WitnetV2.Request storage __request = __seekQueryRequest(_witnetQueryIds[_ix]); - _revenues += __request.evmReward; - _expenses += _gasPrice * __request.unpackCallbackGasLimit(); - } - } - return int256(_revenues) - int256(_expenses); - } - /// Reports the Witnet-provable result to a previously posted request. /// @dev Will assume `block.timestamp` as the timestamp at which the request was solved. /// @dev Fails if: diff --git a/contracts/core/defaults/WitnetRequestBoardTrustableDefault.sol b/contracts/core/defaults/WitnetRequestBoardTrustableDefault.sol index eb71a0cc..408995e4 100644 --- a/contracts/core/defaults/WitnetRequestBoardTrustableDefault.sol +++ b/contracts/core/defaults/WitnetRequestBoardTrustableDefault.sol @@ -16,6 +16,8 @@ contract WitnetRequestBoardTrustableDefault is WitnetRequestBoardTrustableBase { + using WitnetV2 for WitnetV2.Request; + uint256 internal immutable __reportResultGasBase; uint256 internal immutable __reportResultWithCallbackGasBase; uint256 internal immutable __reportResultWithCallbackRevertGasBase; @@ -97,6 +99,35 @@ contract WitnetRequestBoardTrustableDefault } } + /// @notice Estimates the actual earnings (or loss), in WEI, that a reporter would get by reporting result to given query, + /// @notice based on the gas price of the calling transaction. Data requesters should consider upgrading the reward on + /// @notice queries providing no actual earnings. + /// @dev Fails if the query does not exist, or if deleted. + function estimateQueryEarnings(uint256[] calldata _witnetQueryIds, uint256 _gasPrice) + virtual override + external view + returns (int256 _earnings) + { + uint256 _expenses; uint256 _revenues; + for (uint _ix = 0; _ix < _witnetQueryIds.length; _ix ++) { + if (_statusOf(_witnetQueryIds[_ix]) == WitnetV2.QueryStatus.Posted) { + WitnetV2.Request storage __request = __seekQueryRequest(_witnetQueryIds[_ix]); + _revenues += __request.evmReward; + uint96 _callbackGasLimit = __request.unpackCallbackGasLimit(); + if (_callbackGasLimit > 0) { + _expenses += estimateBaseFeeWithCallback(_gasPrice, _callbackGasLimit); + } else { + _expenses += estimateBaseFee( + _gasPrice, + registry.lookupRadonRequestResultMaxSize(__request.RAD) + ); + } + } + } + return int256(_revenues) - int256(_expenses); + } + + // ================================================================================================================ // --- Overrides 'Payable' ---------------------------------------------------------------------------------------- diff --git a/contracts/interfaces/V2/IWitnetRequestBoard.sol b/contracts/interfaces/V2/IWitnetRequestBoard.sol index aa4ec2d1..9ef9ce97 100644 --- a/contracts/interfaces/V2/IWitnetRequestBoard.sol +++ b/contracts/interfaces/V2/IWitnetRequestBoard.sol @@ -141,5 +141,4 @@ interface IWitnetRequestBoard { /// @notice Increments the reward of a previously posted request by adding the transaction value to it. /// @param queryId The unique query identifier. function upgradeQueryReward(uint256 queryId) external payable; - } From 65d617eb04b1c3d22819bc050d402db56f361cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Wed, 27 Dec 2023 14:49:57 +0100 Subject: [PATCH 3/7] feat: Witnet.recoverAddr --- contracts/libs/Witnet.sol | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/contracts/libs/Witnet.sol b/contracts/libs/Witnet.sol index 5b2fcfec..4ab15fc9 100644 --- a/contracts/libs/Witnet.sol +++ b/contracts/libs/Witnet.sol @@ -384,6 +384,34 @@ library Witnet { } + /// =============================================================================================================== + /// --- 'bytes32' helper methods ---------------------------------------------------------------------------------- + + function recoverAddr(bytes32 hash, bytes memory signature) + internal pure + returns (address addr) + { + if (signature.length != 65) { + return (address(0)); + } + bytes32 r; + bytes32 s; + uint8 v; + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return address(0); + } + if (v != 27 && v != 28) { + return address(0); + } + return ecrecover(hash, v, r, s); + } + + /// =============================================================================================================== /// --- 'string' helper methods ----------------------------------------------------------------------------------- From 0bd4766805c4e48549a97e12448d40e85280c2c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Wed, 27 Dec 2023 15:08:05 +0100 Subject: [PATCH 4/7] feat: Witnet.datatType(Witnet.Result) --- contracts/libs/Witnet.sol | 153 ++++++++++++------------------------ contracts/libs/WitnetV2.sol | 2 +- 2 files changed, 50 insertions(+), 105 deletions(-) diff --git a/contracts/libs/Witnet.sol b/contracts/libs/Witnet.sol index 4ab15fc9..59091eb3 100644 --- a/contracts/libs/Witnet.sol +++ b/contracts/libs/Witnet.sol @@ -466,55 +466,8 @@ library Witnet { } } - - // /// =============================================================================================================== - // /// --- 'Witnet.Request' helper methods --------------------------------------------------------------------------- - - // function packRequesterCallbackGasLimit(address requester, uint96 callbackGasLimit) internal pure returns (bytes32) { - // return bytes32(uint(bytes32(bytes20(requester))) | callbackGasLimit); - // } - - // function unpackRequester(Request storage self) internal view returns (address) { - // return address(bytes20(self.fromCallbackGas)); - // } - - // function unpackCallbackGasLimit(Request storage self) internal view returns (uint96) { - // return uint96(uint(self.fromCallbackGas)); - // } - - // function unpackRequesterAndCallbackGasLimit(Request storage self) internal view returns (address, uint96) { - // bytes32 _packed = self.fromCallbackGas; - // return (address(bytes20(_packed)), uint96(uint(_packed))); - // } - - - // /// =============================================================================================================== - // /// --- 'Witnet.Response' helper methods -------------------------------------------------------------------------- - - // function packReporterEvmFinalityBlock(address reporter, uint256 evmFinalityBlock) internal pure returns (bytes32) { - // return bytes32(uint(bytes32(bytes20(reporter))) << 96 | uint96(evmFinalityBlock)); - // } - - // function unpackWitnetReporter(Response storage self) internal view returns (address) { - // return address(bytes20(self.fromFinality)); - // } - - // function unpackEvmFinalityBlock(Response storage self) internal view returns (uint256) { - // return uint(uint96(uint(self.fromFinality))); - // } - - // function unpackEvmFinalityBlock(bytes32 fromFinality) internal pure returns (uint256) { - // return uint(uint96(uint(fromFinality))); - // } - - // function unpackWitnetReporterAndEvmFinalityBlock(Response storage self) internal view returns (address, uint256) { - // bytes32 _packed = self.fromFinality; - // return (address(bytes20(_packed)), uint(uint96(uint(_packed)))); - // } - - /// =============================================================================================================== - /// --- 'Witnet.Result' helper methods ---------------------------------------------------------------------------- + /// --- Witnet.* helper methods ----------------------------------------------------------------------------------- modifier _isReady(Result memory result) { require(result.success, "Witnet: tried to decode value from errored result."); @@ -673,62 +626,54 @@ library Witnet { return result.value.readUintArray(); } + function dataType(Witnet.Result memory result) + internal pure + returns (Witnet.RadonDataTypes) + { + uint8 _majorType = result.value.majorType; + if (_majorType == 0 || _majorType == 1) { + return Witnet.RadonDataTypes.Integer; + } + if (_majorType == 2) { + return Witnet.RadonDataTypes.Bytes; + } else if (_majorType == 3) { + return Witnet.RadonDataTypes.String; + } else if (_majorType == 4) { + return Witnet.RadonDataTypes.Array; + } else if (_majorType == 5) { + return Witnet.RadonDataTypes.Map; + } else if (_majorType == 7) { + if (result.value.additionalInformation == 20 || result.value.additionalInformation ==21) { + return Witnet.RadonDataTypes.Bool; + } else if (result.value.additionalInformation >= 25 && result.value.additionalInformation <= 27) { + return Witnet.RadonDataTypes.Float; + } else { + return Witnet.RadonDataTypes.Any; + } + } else { + return Witnet.RadonDataTypes.Any; + } + } - // /// =============================================================================================================== - // /// --- 'Witnet.RadonSLA' helper methods -------------------------------------------------------------------------- - - // /// @notice Returns `true` if all witnessing parameters in `b` have same - // /// @notice value or greater than the ones in `a`. - // function equalOrGreaterThan(RadonSLA memory a, RadonSLA memory b) - // internal pure returns (bool) - // { - // return ( - // a.numWitnesses >= b.numWitnesses - // && a.minConsensusPercentage >= b.minConsensusPercentage - // && a.witnessReward >= b.witnessReward - // && a.witnessCollateral >= b.witnessCollateral - // && a.minerCommitRevealFee >= b.minerCommitRevealFee - // ); - // } - - // function isValid(Witnet.RadonSLA memory sla) - // internal pure returns (bool) - // { - // return ( - // sla.witnessReward > 0 - // && sla.numWitnesses > 0 && sla.numWitnesses <= 127 - // && sla.minConsensusPercentage > 50 && sla.minConsensusPercentage < 100 - // && sla.witnessCollateral > 0 - // && sla.witnessCollateral / sla.witnessReward <= 127 - // ); - // } - - // function toBytes32(RadonSLA memory sla) internal pure returns (bytes32) { - // return bytes32( - // uint(sla.witnessReward) - // | sla.witnessCollateral << 64 - // | sla.minerCommitRevealFee << 128 - // | sla.numWitnesses << 248 - // | sla.minConsensusPercentage << 232 - // ); - // } - - // function toRadonSLA(bytes32 _packed) internal pure returns (RadonSLA memory) { - // return RadonSLA({ - // numWitnesses: uint8(uint(_packed >> 248)), - // minConsensusPercentage: uint8(uint(_packed >> 232) & 0xff), - // witnessReward: uint64(uint(_packed) & 0xffffffffffffffff), - // witnessCollateral: uint64(uint(_packed >> 64) & 0xffffffffffffffff), - // minerCommitRevealFee: uint64(uint(_packed >> 128) & 0xffffffffffffffff) - // }); - // } - - // function witnessingWitTotalReward(Witnet.RadonSLA memory sla) - // internal pure returns (uint64) - // { - // return sla.witnessReward * sla.numWitnesses; - // } - + function toString(Witnet.RadonDataTypes _dataType) internal pure returns (string memory) { + if (_dataType == RadonDataTypes.Integer) { + return "integer"; + } else if (_dataType == RadonDataTypes.String) { + return "string"; + } else if (_dataType == RadonDataTypes.Map) { + return "object"; + } else if (_dataType == RadonDataTypes.Array) { + return "array"; + } else if (_dataType == RadonDataTypes.Bool) { + return "boolean"; + } else if (_dataType == RadonDataTypes.Float) { + return "float"; + } else if (_dataType == RadonDataTypes.Subscript) { + return "script"; + } else { + return "any"; + } + } /// =============================================================================================================== /// --- Witnet library private methods ---------------------------------------------------------------------------- @@ -758,4 +703,4 @@ library Witnet { } } } -} \ No newline at end of file +} diff --git a/contracts/libs/WitnetV2.sol b/contracts/libs/WitnetV2.sol index 13e0f128..f219fe91 100644 --- a/contracts/libs/WitnetV2.sol +++ b/contracts/libs/WitnetV2.sol @@ -36,7 +36,7 @@ library WitnetV2 { /// Data kept in EVM-storage containing Witnet-provided response metadata and result. struct Response { bytes32 fromFinality; // Packed: contains address from which the result to the data request was reported, and - // the EVM block at which the provided result can be considered to be final. + // the EVM block at which the provided result can be considered to be final. uint256 timestamp; // Timestamp at which data from data sources were retrieved by the Witnet blockchain. bytes32 tallyHash; // Hash of the Witnet commit/reveal act that solved the data request. bytes cborBytes; // CBOR-encoded result to the data request, as resolved by the Witnet blockchain. From 94d2ce02cce0ee133381f660f3beffe889062468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Wed, 27 Dec 2023 15:10:16 +0100 Subject: [PATCH 5/7] feat: WitnetTraps specs --- contracts/WitnetTraps.sol | 21 +++++ contracts/interfaces/V2/IWitnetTraps.sol | 87 +++++++++++++++++++ .../interfaces/V2/IWitnetTrapsEvents.sol | 11 +++ 3 files changed, 119 insertions(+) create mode 100644 contracts/WitnetTraps.sol create mode 100644 contracts/interfaces/V2/IWitnetTraps.sol create mode 100644 contracts/interfaces/V2/IWitnetTrapsEvents.sol diff --git a/contracts/WitnetTraps.sol b/contracts/WitnetTraps.sol new file mode 100644 index 00000000..84818df7 --- /dev/null +++ b/contracts/WitnetTraps.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0 <0.9.0; + +import "./WitnetBytecodes.sol"; +import "./interfaces/V2/IWitnetTraps.sol"; +import "./interfaces/V2/IWitnetTrapsEvents.sol"; + +/// @title WitnetTraps: Witnet Push Oracle base contract +/// @author The Witnet Foundation. +abstract contract WitnetTraps + is + IWitnetTraps, + IWitnetTrapsEvents +{ + function class() virtual external view returns (string memory) { + return type(WitnetTraps).name; + } + function registry() virtual external view returns (WitnetBytecodes); + function specs() virtual external view returns (bytes4); +} diff --git a/contracts/interfaces/V2/IWitnetTraps.sol b/contracts/interfaces/V2/IWitnetTraps.sol new file mode 100644 index 00000000..d350a37f --- /dev/null +++ b/contracts/interfaces/V2/IWitnetTraps.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.7.0 <0.9.0; + +import "../../libs/WitnetV2.sol"; + +interface IWitnetTraps { + + error NoTrap(); + error NoValue(); + error ExpiredValue(); + + struct DataPoint { + bytes drTallyCborBytes; + bytes16 drTrapHash; + uint64 drTimestamp; + uint64 finalityBlock; + } + + struct SLA { + bytes32 radHash; + uint64 maxGasPrice; + uint64 maxTimestamp; + uint32 heartbeatSecs; + uint32 cooldownSecs; + uint16 reportFee10000; + uint16 deviationThreshold10000; + uint16 maxResultSize; + uint8 minWitnesses; + Witnet.RadonDataTypes dataType; + } + + struct TrapInfo { + bytes4 feedId; + address feeder; + uint256 balance; + bytes bytecode; + string dataType; + DataPoint lastData; + SLA trapSLA; + } + + struct TrapReport { + bytes32 drRadHash; + bytes drTallyCborBytes; + uint64 drTimestamp; + uint16 drWitnesses; + bytes32 trapId; + } + + enum TrapReportStatus { + Unknown, + Reported, + ExcessiveGasPrice, + InsufficientBalance, + InsufficientCooldown, + InsufficientDeviation, + InsufficientWitnesses, + InvalidRadHash, + InvalidResult, + InvalidSignature, + InvalidTimestamp, + PreviousValueNotFinalized + } + + receive() external payable; + + function balanceOf(address feeder) external view returns (uint256); + function estimateBaseFee(uint256 gasPrice, uint16 maxResultSize) external view returns (uint256); + + function fund(address feeder) external payable returns (uint256 newBalance); + + function getActiveTrapInfo(bytes32 trapId) external view returns (TrapInfo memory); + function getActiveTrapsCount() external view returns (uint64); + function getActiveTrapsRange(uint64 offset, uint64 length) external view returns (bytes32[] memory, TrapInfo[] memory); + + function getDataFeedLastUpdate(address feeder, bytes4 dataFeedId) external view returns (DataPoint memory); + function getDataFeedLastUpdateUnsafe(address feeder, bytes4 dataFeedId) external view returns (DataPoint memory); + function getDataFeedTrapSLA(address feeder, bytes4 dataFeedId) external view returns (SLA memory); + + function trapDataFeed(bytes4 dataFeedId, SLA calldata trapSLA) external payable returns (uint256 newBalance); + function untrapDataFeed(bytes4 dataFeedId) external returns (DataPoint memory); + + function reportDataFeeds(TrapReport[] calldata reports) external returns (TrapReportStatus[] memory, uint256 totalEvmReward); + function reportDataFeeds(TrapReport[] calldata reports, bytes[] calldata signatures) external returns (TrapReportStatus[] memory); + + function withdraw() external returns (uint256 withdrawn); +} diff --git a/contracts/interfaces/V2/IWitnetTrapsEvents.sol b/contracts/interfaces/V2/IWitnetTrapsEvents.sol new file mode 100644 index 00000000..b8d97517 --- /dev/null +++ b/contracts/interfaces/V2/IWitnetTrapsEvents.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.7.0 <0.9.0; + +import "./IWitnetTraps.sol"; + +interface IWitnetTrapsEvents { + event Funded(address indexed to, uint256 newBalance); + event Rewarded(address indexed from, address indexed to, uint256 reward); + event Withdrawn(address indexed by, uint256 withdrawn); + event Trap(bytes32 indexed id, address indexed feeder, IWitnetTraps.SLA sla); +} From fc24ff02a56998ce0c9aab1ef7f0a956531a7e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Wed, 27 Dec 2023 15:10:36 +0100 Subject: [PATCH 6/7] feat: first approach to WitnetTrapsTrustable* --- .../defaults/WitnetTrapsTrustableBase.sol | 619 ++++++++++++++++++ .../defaults/WitnetTrapsTrustableDefault.sol | 91 +++ contracts/data/WitnetTrapsData.sol | 108 +++ 3 files changed, 818 insertions(+) create mode 100644 contracts/core/defaults/WitnetTrapsTrustableBase.sol create mode 100644 contracts/core/defaults/WitnetTrapsTrustableDefault.sol create mode 100644 contracts/data/WitnetTrapsData.sol diff --git a/contracts/core/defaults/WitnetTrapsTrustableBase.sol b/contracts/core/defaults/WitnetTrapsTrustableBase.sol new file mode 100644 index 00000000..af3aa363 --- /dev/null +++ b/contracts/core/defaults/WitnetTrapsTrustableBase.sol @@ -0,0 +1,619 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.7.0 <0.9.0; +pragma experimental ABIEncoderV2; + +import "../WitnetUpgradableBase.sol"; +import "../../WitnetBytecodes.sol"; +import "../../WitnetTraps.sol"; + +import "../../data/WitnetTrapsData.sol"; +import "../../interfaces/IWitnetRequestBoardAdminACLs.sol"; + +/// @title Witnet Traps Board "trustable" base implementation contract. +/// @notice Contract where to subscribe PUSH data traps, and where PUSH data can get eventually reported +/// @notice as long as data gets externally signed by pre-authorized (trustable) authorities. +/// @author The Witnet Foundation +abstract contract WitnetTrapsTrustableBase + is + WitnetUpgradableBase, + WitnetTraps, + WitnetTrapsData, + IWitnetRequestBoardAdminACLs +{ + using Witnet for Witnet.RadonDataTypes; + using Witnet for Witnet.Result; + + bytes4 public immutable override specs = type(IWitnetTraps).interfaceId; + WitnetBytecodes immutable public override registry; + + constructor( + WitnetBytecodes _registry, + bool _upgradable, + bytes32 _versionTag + ) + WitnetUpgradableBase( + _upgradable, + _versionTag, + "io.witnet.proxiable.traps" + ) + { + registry = _registry; + } + + /// @dev Provide backwards compatibility for dapps bound to versions <= 0.6.1 + /// @dev (i.e. calling methods in IWitnetRequestBoard) + /// @dev (Until 'function ... abi(...)' modifier is allegedly supported in solc versions >= 0.9.1) + /* solhint-disable payable-fallback */ + /* solhint-disable no-complex-fallback */ + fallback() override external { + revert(string(abi.encodePacked( + "WitnetTrapsTrustableBase: not implemented: 0x", + Witnet.toHexString(uint8(bytes1(msg.sig))), + Witnet.toHexString(uint8(bytes1(msg.sig << 8))), + Witnet.toHexString(uint8(bytes1(msg.sig << 16))), + Witnet.toHexString(uint8(bytes1(msg.sig << 24))) + ))); + } + + + // ================================================================================================================ + // --- Yet to be implemented virtual methods ---------------------------------------------------------------------- + + function _blockNumber() virtual internal view returns (uint64); + function _blockTimestamp() virtual internal view returns (uint64); + function _hashTrapReport(TrapReport calldata) virtual internal pure returns (bytes16); + function estimateBaseFee(uint256 gasPrice, uint16 maxResultSize) virtual public view returns (uint256); + + + // ================================================================================================================ + // --- Overrides 'Upgradeable' ------------------------------------------------------------------------------------ + + /// @notice Re-initialize contract's storage context upon a new upgrade from a proxy. + /// @dev Must fail when trying to upgrade to same logic contract more than once. + function initialize(bytes memory _initData) + public + override + { + address _owner = owner(); + if (_owner == address(0)) { + // get owner (and reporters) from _initData + bytes memory _reportersRaw; + (_owner, _reportersRaw) = abi.decode(_initData, (address, bytes)); + _transferOwnership(_owner); + __setReporters(abi.decode(_reportersRaw, (address[]))); + } else { + // only owner can initialize: + require( + msg.sender == _owner, + "WitnetTrapsTrustableBase: not the owner" + ); + // get reporters from _initData + __setReporters(abi.decode(_initData, (address[]))); + } + + if (__storage().base != address(0)) { + // current implementation cannot be initialized more than once: + require( + __storage().base != base(), + "WitnetTrapsTrustableBase: already upgraded" + ); + } + __storage().base = base(); + + require( + address(registry).code.length > 0, + "WitnetTrapsTrustableBase: inexistent registry" + ); + require( + registry.specs() == type(IWitnetBytecodes).interfaceId, + "WitnetTrapsTrustableBase: uncompliant registry" + ); + + emit Upgraded(_owner, base(), codehash(), version()); + } + + /// Tells whether provided address could eventually upgrade the contract. + function isUpgradableFrom(address _from) external view override returns (bool) { + address _owner = owner(); + return ( + // false if the WRB is intrinsically not upgradable, or `_from` is no owner + isUpgradable() + && _owner == _from + ); + } + + + // ================================================================================================================ + // --- Full implementation of 'IWitnetRequestBoardAdminACLs' ------------------------------------------------------ + + /// Tells whether given address is included in the active reporters control list. + /// @param _reporter The address to be checked. + function isReporter(address _reporter) public view override returns (bool) { + return __storage().isReporter[_reporter]; + } + + /// Adds given addresses to the active reporters control list. + /// @dev Can only be called from the owner address. + /// @dev Emits the `ReportersSet` event. + /// @param _reporters List of addresses to be added to the active reporters control list. + function setReporters(address[] memory _reporters) + public + override + onlyOwner + { + __setReporters(_reporters); + } + + /// Removes given addresses from the active reporters control list. + /// @dev Can only be called from the owner address. + /// @dev Emits the `ReportersUnset` event. + /// @param _exReporters List of addresses to be added to the active reporters control list. + function unsetReporters(address[] memory _exReporters) + public + override + onlyOwner + { + for (uint ix = 0; ix < _exReporters.length; ix ++) { + address _reporter = _exReporters[ix]; + __storage().isReporter[_reporter] = false; + } + emit ReportersUnset(_exReporters); + } + + + // ================================================================================================================ + // --- 'IWitnetTraps' --------------------------------------------------------------------------------------------- + + receive() virtual override external payable { + emit Funded(msg.sender, __fund(msg.sender)); + } + + function balanceOf(address _feeder) + virtual override + external view + returns (uint256) + { + return __storage().balances[_feeder]; + } + + function fund(address _feeder) + virtual override + public payable + returns (uint256 _newBalance) + { + _newBalance = __fund(_feeder); + emit Funded(_feeder, _newBalance); + } + + function getActiveTrapInfo(bytes32 _trapId) + virtual override + external view + returns (TrapInfo memory) + { + TrapStorage storage __trap = __seekTrap(_trapId); + if (__trap.feeder == address(0)) { + revert NoTrap(); + } else { + return _toTrapInfo(__trap); + } + } + + function getActiveTrapsCount() + virtual override + external view + returns (uint64) + { + return uint64(__storage().trapIds.length); + } + + function getActiveTrapsRange(uint64 _offset, uint64 _length) + virtual override + external view + returns (bytes32[] memory _ids, TrapInfo[] memory _traps) + { + uint64 _totalTraps = uint64(__storage().trapIds.length); + require( + _offset < _totalTraps, + "WitnetTraps: offset out of range" + ); + if (_offset + _length > _totalTraps) { + _length = _totalTraps - _offset; + } + _ids = new bytes32[](_length); + _traps = new TrapInfo[](_length); + for (uint64 _ix = 0; _ix < _length; _ix ++) { + _ids[_ix] = __storage().trapIds[_offset + _ix]; + _traps[_ix] = _toTrapInfo(__seekTrap(_ids[_ix])); + } + } + + function getDataFeedLastUpdate(address _feeder, bytes4 _dataFeedId) + virtual override + external view + returns (DataPoint memory _data) + { + _data = getDataFeedLastUpdateUnsafe(_feeder, _dataFeedId); + TrapStorage storage __trap = __seekTrap(_feeder, _dataFeedId); + if ( + __trap.sla.heartbeatSecs > 0 + && _blockTimestamp() > _data.drTimestamp + __trap.sla.heartbeatSecs + ) { + revert ExpiredValue(); + } + } + + function getDataFeedLastUpdateUnsafe(address _feeder, bytes4 _dataFeedId) + virtual override + public view + returns (DataPoint memory _data) + { + TrapStorage storage __trap = __seekTrap(_feeder, _dataFeedId); + uint _finalityBlock = _extractDataPointFinalityBlock(__trap.dataPtr); + if (__trap.feeder == address(0)) { + revert NoTrap(); + } else if (_finalityBlock == 0) { + revert NoValue(); + } + if (_blockNumber() < _finalityBlock) { + _data = _extractDataPoint(__trap.prevDataPtr); + } else { + _data = _extractDataPoint(__trap.dataPtr); + } + } + + function getDataFeedTrapSLA(address _feeder, bytes4 _dataFeedId) + virtual override + external view + trapIsOwned(_feeder, _dataFeedId) + returns (IWitnetTraps.SLA memory) + { + return __seekTrap(_feeder, _dataFeedId).sla; + } + + function trapDataFeed( + bytes4 _dataFeedId, + IWitnetTraps.SLA calldata _trapSLA + ) + virtual override + external payable + returns (uint256 _newBalance) + { + if (msg.value > 0) { + _newBalance = fund(msg.sender); + } + bytes32 _trapId = _hashTrap(msg.sender, _dataFeedId); + TrapStorage storage __trap = __seekTrap(_trapId); + if (__trap.feeder == address(0)) { + // push new trapId value to storage array: + __storage().trapIds.push(_trapId); + __trap.index = uint64(__storage().trapIds.length) - 1; + // initialize data storage pointers: + __trap.dataPtr = keccak256(abi.encode( + _WITNET_TRAPS_DATA_SLOTHASH, + _trapId, + uint(0) + )); + __trap.prevDataPtr = keccak256(abi.encode( + _WITNET_TRAPS_DATA_SLOTHASH, + _trapId, + uint(1) + )); + } else { + // if RAD Hash changes, makes sure that the data type prevails + if (_trapSLA.radHash != __trap.sla.radHash) { + require( + registry.lookupRadonRequestResultDataType(_trapSLA.radHash) + == registry.lookupRadonRequestResultDataType(__trap.sla.radHash), + "WitnetTraps: data types mistmatch" + ); + } + } + + // validate SLA parameters: + _validateTrapSLA(_trapSLA); + + // sava/update SLA into storage: + __trap.sla = _trapSLA; + + // emit event: + emit Trap(_trapId, msg.sender, _trapSLA); + } + + function untrapDataFeed(bytes4 _dataFeedId) + virtual override + external + trapIsOwned(msg.sender, _dataFeedId) + returns (DataPoint memory _lastData) + { + bytes32 _trapId = _hashTrap(msg.sender, _dataFeedId); + TrapStorage storage __trap = __seekTrap(_trapId); + + // remove trapId from storage array: + __storage().trapIds[__trap.index] = __storage().trapIds[__storage().trapIds.length - 1]; + __storage().trapIds.pop(); + + // extact last known data point: + _lastData = _extractDataPoint(__trap.dataPtr); + + // delete Trap from storage (but not previous values, if any): + delete __storage().traps[_trapId]; + + // emit event: + IWitnetTraps.SLA memory _emptySLA; + emit Trap(_trapId, msg.sender, _emptySLA); + } + + function reportDataFeeds(TrapReport[] calldata _reports) + virtual override + external + nonReentrant + onlyReporters + returns (TrapReportStatus[] memory _status_, uint256 _totalEvmReward) + { + _status_ = new TrapReportStatus[](_reports.length); + address _feeder; + uint256 _feederBalance; + for (uint _ix = 0; _ix < _reports.length; _ix ++) { + TrapStorage storage __trap = __seekTrap(_reports[_ix].trapId); + TrapReportStatus _status = TrapReportStatus.Unknown; + if (__trap.feeder != address(0)) { + if (_feeder == address(0)) { + _feeder = __trap.feeder; + _feederBalance = __storage().balances[_feeder]; + } else if (_feeder != __trap.feeder) { + revert("WitnetTraps: disjoint reports"); + } + _status = __reportDataFeed(_reports[_ix], __trap); + if (_status == IWitnetTraps.TrapReportStatus.Reported) { + uint256 _evmReward = _calcEvmReward( + tx.gasprice, + uint16(_reports[_ix].drTallyCborBytes.length), + __trap.sla.reportFee10000 + ); + if (_totalEvmReward + _evmReward > _feederBalance) { + _status = TrapReportStatus.InsufficientBalance; + } else { + _totalEvmReward += _evmReward; + __saveDataPoint(_reports[_ix], __trap); + } + } + } + _status_[_ix] = _status; + } + if (_feeder != msg.sender) { + __storage().balances[_feeder] -= _totalEvmReward; + __storage().balances[msg.sender] += _totalEvmReward; + emit Rewarded(_feeder, msg.sender, _totalEvmReward); + } + } + + function reportDataFeeds( + TrapReport[] calldata _reports, + bytes[] calldata _signatures + ) + virtual override + external + nonReentrant + returns (TrapReportStatus[] memory _status_) + { + _status_ = new TrapReportStatus[](_reports.length); + for (uint _ix = 0; _ix < _reports.length; _ix ++) { + TrapStorage storage __trap = __seekTrap(_reports[_ix].trapId); + TrapReportStatus _status = TrapReportStatus.Unknown; + if (__trap.feeder != address(0)) { + _status = __reportDataFeed(_reports[_ix], __trap); + if (_status == IWitnetTraps.TrapReportStatus.Reported) { + bytes32 _reportHash = _hashTrapReport(_reports[_ix]); + if (!__storage().isReporter[Witnet.recoverAddr(_reportHash, _signatures[_ix])]) { + _status = IWitnetTraps.TrapReportStatus.InvalidSignature; + } else { + __saveDataPoint(_reports[_ix], __trap); + } + } + } + _status_[_ix] = _status; + } + } + + function withdraw() + virtual override + external + returns (uint256 _withdrawn) + { + _withdrawn = __storage().balances[msg.sender]; + __storage().balances[msg.sender] = 0; + emit Withdrawn( + msg.sender, + __safeTransferTo( + payable(msg.sender), + _withdrawn + ) + ); + } + + + // ================================================================================================================ + // --- Virtual and internal functions ----------------------------------------------------------------------------- + + function _calcEvmReward(uint256 _gasPrice, uint16 _maxResultSize, uint16 _reportFee10000) + virtual internal view + returns (uint256) + { + return ( + estimateBaseFee(_gasPrice, _maxResultSize) + * ( + 10000 + + _reportFee10000 + ) + ) / 10000; + } + + function _calcResultDeviation10000( + Witnet.Result memory _result, + TrapStorage storage __trap + ) + internal view + returns (uint64) + { + if (__trap.sla.dataType == Witnet.RadonDataTypes.Integer) { + int64 _current = int64(Witnet.asInt( + Witnet.resultFromCborBytes( + _extractDataPoint(__trap.dataPtr).drTallyCborBytes + ) + )); + int64 _diff = int64(Witnet.asInt(_result)) - _current; + if (_diff < 0) _diff *= -1; + return uint64(_diff * 10000) / uint64(_current); + } else if ( + keccak256(_result.value.buffer.data) + != keccak256(_extractDataPoint(__trap.dataPtr).drTallyCborBytes) + ) { + return 1; + } else { + return 0; + } + } + + function _toTrapInfo(TrapStorage storage __trap) + virtual internal view + returns (IWitnetTraps.TrapInfo memory) + { + return TrapInfo({ + feedId: __trap.feedId, + feeder: __trap.feeder, + balance: __storage().balances[__trap.feeder], + bytecode: registry.bytecodeOf(__trap.sla.radHash), + dataType: __trap.sla.dataType.toString(), + lastData: ( + _blockNumber() >= _extractDataPointFinalityBlock(__trap.dataPtr) + ? _extractDataPoint(__trap.dataPtr) + : _extractDataPoint(__trap.prevDataPtr) + ), + trapSLA: __trap.sla + }); + } + + function _validateTrapSLA(IWitnetTraps.SLA calldata sla) virtual internal view { + require(sla.radHash != 0, "WitnetTraps: no RAD hash?"); + require(sla.maxGasPrice > 0, "WitnetTraps: no max gas price?"); + require(sla.maxResultSize > 0, "WitnetTraps: no result size?"); + require(sla.minWitnesses > 0, "WitnetTraps: no witnesses?"); + require( + (sla.heartbeatSecs == 0 && sla.cooldownSecs == 0) + || sla.heartbeatSecs >= sla.cooldownSecs, + "WitnetTraps: invalid heartbeat" + ); + Witnet.RadonDataTypes _radDataType = registry.lookupRadonRequestResultDataType(sla.radHash); + require(sla.dataType == _radDataType, "WitnetTraps: RAD data type mismatch"); + if ( + _radDataType != Witnet.RadonDataTypes.Integer + && sla.deviationThreshold10000 > 1 + ) { + revert("WitnetTraps: invalid deviation threshold"); + } + } + + function __fund(address _feeder) + virtual internal + returns (uint256 _newBalance) + { + _newBalance = __storage().balances[_feeder] + msg.value; + __storage().balances[_feeder] = _newBalance; + } + + function __reportDataFeed( + TrapReport calldata _report, + TrapStorage storage __trap + ) + virtual internal + returns (TrapReportStatus _status) + { + // DataPoint storage __lastData = _extractDataPoint(__trap.dataPtr); + (uint64 _lastFinalityBlock, uint64 _lastDrTimestamp) = _extractDataPointFinalityBlockAndTimestamp(__trap.dataPtr); + Witnet.Result memory _result = Witnet.resultFromCborBytes(_report.drTallyCborBytes); + IWitnetTraps.SLA memory _sla = __trap.sla; + + int32 _elapsedSecs = int32(int64(_report.drTimestamp) - int64(_lastDrTimestamp)); + if ( + !_result.success + || _result.dataType() != _sla.dataType + || ( + _sla.maxResultSize != 0 + && _report.drTallyCborBytes.length > _sla.maxResultSize + ) + ) { + _status = TrapReportStatus.InvalidResult; + } else if (_blockNumber() < _lastFinalityBlock) { + _status = TrapReportStatus.PreviousValueNotFinalized; + } else if (_report.drRadHash != _sla.radHash) { + _status = TrapReportStatus.InvalidRadHash; + } else if ( + _lastDrTimestamp != 0 && _elapsedSecs > 0 + || ( + _sla.maxTimestamp > 0 + && _blockTimestamp() > _sla.maxTimestamp + ) + ) { + _status = TrapReportStatus.InvalidTimestamp; + } else if (_report.drWitnesses < _sla.minWitnesses) { + _status = TrapReportStatus.InsufficientWitnesses; + } else if (_sla.cooldownSecs > 0 && _elapsedSecs < int32(_sla.cooldownSecs)) { + _status = TrapReportStatus.InsufficientCooldown; + } else if ( + _sla.deviationThreshold10000 > 0 + && _calcResultDeviation10000(_result, __trap) < _sla.deviationThreshold10000 + && ( + _sla.heartbeatSecs == 0 + || _elapsedSecs < int32(_sla.heartbeatSecs) + ) + ) { + _status = TrapReportStatus.InsufficientDeviation; + } else if ( + _sla.maxGasPrice > 0 + && tx.gasprice > _sla.maxGasPrice + ) { + _status = TrapReportStatus.ExcessiveGasPrice; + } else { + _status = TrapReportStatus.Reported; + } + } + + /// Transfers ETHs to given address. + /// @param _to Recipient address. + /// @param _quantity Amount of ETHs to transfer. + function __safeTransferTo(address payable _to, uint256 _quantity) + virtual + internal + returns (uint256) + { + _to.transfer(_quantity); + return _quantity; + } + + function __saveDataPoint(TrapReport calldata _report, TrapStorage storage __trap) + virtual internal + { + bytes32 _dataPtr = __trap.prevDataPtr; + __trap.prevDataPtr = __trap.dataPtr; + __trap.dataPtr = _dataPtr; + DataPointPacked storage __datapoint = __seekDataPointPacked(_dataPtr); + __datapoint.drTallyCborBytes = _report.drTallyCborBytes; + __datapoint.packed = ( + bytes32(_hashTrapReport(_report) << 128) + | bytes32(uint(_report.drTimestamp) << 64) + | bytes32(uint(_blockNumber())) + ); + } + + function __setReporters(address[] memory _reporters) + virtual internal + { + for (uint ix = 0; ix < _reporters.length; ix ++) { + address _reporter = _reporters[ix]; + __storage().isReporter[_reporter] = true; + } + emit ReportersSet(_reporters); + } + +} \ No newline at end of file diff --git a/contracts/core/defaults/WitnetTrapsTrustableDefault.sol b/contracts/core/defaults/WitnetTrapsTrustableDefault.sol new file mode 100644 index 00000000..d727af4c --- /dev/null +++ b/contracts/core/defaults/WitnetTrapsTrustableDefault.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT + +/* solhint-disable var-name-mixedcase */ + +pragma solidity >=0.8.0 <0.9.0; + +import "./WitnetTrapsTrustableBase.sol"; + +/// @title Witnet Request Board "trustable" implementation contract. +/// @notice Contract to bridge requests to Witnet Decentralized Oracle Network. +/// @dev This contract enables posting requests that Witnet bridges will insert into the Witnet network. +/// The result of the requests will be posted back to this contract by the bridge nodes too. +/// @author The Witnet Foundation +contract WitnetTrapsTrustableDefault + is + WitnetTrapsTrustableBase +{ + using WitnetV2 for WitnetV2.Request; + + uint256 internal immutable __reportResultGasBase; + uint256 internal immutable __sstoreFromZeroGas; + + constructor( + WitnetBytecodes _registry, + bool _upgradable, + bytes32 _versionTag, + uint256 _reportResultGasBase, + uint256 _sstoreFromZeroGas + ) + WitnetTrapsTrustableBase( + _registry, + _upgradable, + _versionTag + ) + { + __reportResultGasBase = _reportResultGasBase; + __sstoreFromZeroGas = _sstoreFromZeroGas; + } + + + // ================================================================================================================ + // --- Overrides 'WitnetTrapsTrustableBase' ----------------------------------------------------------------------- + + function _blockNumber() + virtual override + internal view + returns (uint64) + { + return uint64(block.number); + } + + function _blockTimestamp() + virtual override + internal view + returns (uint64) + { + return uint64(block.timestamp); + } + + function _hashTrapReport(TrapReport calldata report) + virtual override + internal pure + returns (bytes16) + { + return bytes16(keccak256(abi.encode( + report.drRadHash, + report.drTimestamp, + report.drWitnesses, + report.drTallyCborBytes + ))); + } + + + // ================================================================================================================ + // --- Overrides 'IWitnetTraps' ----------------------------------------------------------------------------------- + + function estimateBaseFee(uint256 gasPrice, uint16 maxResultSize) + virtual override + public view + returns (uint256) + { + return gasPrice * ( + __reportResultGasBase + + __sstoreFromZeroGas * ( + 5 + (maxResultSize == 0 ? 0 : maxResultSize - 1) / 32 + ) + ); + } + +} + \ No newline at end of file diff --git a/contracts/data/WitnetTrapsData.sol b/contracts/data/WitnetTrapsData.sol new file mode 100644 index 00000000..2de002a5 --- /dev/null +++ b/contracts/data/WitnetTrapsData.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.7.0 <0.9.0; + +import "../interfaces/V2/IWitnetTraps.sol"; + +/// @title Witnet Request Board base data model. +/// @author The Witnet Foundation. +abstract contract WitnetTrapsData { + + using WitnetV2 for WitnetV2.Request; + + bytes32 internal constant _WITNET_TRAPS_DATA_SLOTHASH = + /* keccak256("io.witnet.traps.data") */ + 0xb44c98929a7f33726003754788fddc0e0449de5db2ff77e156bc994df2bb5916; + + struct Storage { + address base; + mapping (address => uint256) balances; + mapping (address => bool) isReporter; + mapping (bytes32 => TrapStorage) traps; + bytes32[] trapIds; + } + + struct TrapStorage { + uint64 index; + bytes4 feedId; + address feeder; + bytes32 dataPtr; + bytes32 prevDataPtr; + IWitnetTraps.SLA sla; + } + + struct DataPointPacked { + bytes drTallyCborBytes; + bytes32 packed; + } + + modifier onlyReporters { + require( + __storage().isReporter[msg.sender], + "WitnetTraps: unauthorized reporter" + ); _; + } + + modifier trapIsOwned(address feeder, bytes4 feedId) { + if (__seekTrap(feeder, feedId).feeder != feeder) { + revert IWitnetTraps.NoTrap(); + } _; + } + + constructor() {} + + // ================================================================================================================ + // --- Internal functions ----------------------------------------------------------------------------------------- + + function _extractDataPoint(bytes32 _storagePtr) internal view returns (IWitnetTraps.DataPoint memory) { + DataPointPacked storage __data = __seekDataPointPacked(_storagePtr); + bytes32 _packed = __data.packed; + return IWitnetTraps.DataPoint({ + drTallyCborBytes: __data.drTallyCborBytes, + drTrapHash: bytes16(_packed >> 128), + drTimestamp: uint64(uint(_packed >> 192)), + finalityBlock: uint64(uint(_packed)) + }); + } + + function _extractDataPointFinalityBlock(bytes32 _storagePtr) internal view returns (uint64) { + return uint64(uint(__seekDataPointPacked(_storagePtr).packed)); + } + + function _extractDataPointFinalityBlockAndTimestamp(bytes32 _storagePtr) internal view returns (uint64, uint64) { + bytes32 _packed = __seekDataPointPacked(_storagePtr).packed; + return ( + uint64(uint(_packed)), + uint64(uint(_packed) >> 64) + ); + } + + function _hashTrap(address trapper, bytes4 feedId) internal pure returns (bytes32) { + return keccak256(abi.encode(trapper, feedId)); + } + + function __seekDataPointPacked(bytes32 _storagePtr) internal pure returns (DataPointPacked storage data) { + assembly { + data.slot := _storagePtr + } + } + + /// Gets trap storage by query id. + function __seekTrap(bytes32 trapId) internal view returns (TrapStorage storage) { + return __storage().traps[trapId]; + } + + function __seekTrap(address trapper, bytes4 dataFeedId) + internal view + returns (TrapStorage storage) + { + return __storage().traps[_hashTrap(trapper, dataFeedId)]; + } + + /// Returns storage pointer to contents of 'WitnetBoardState' struct. + function __storage() internal pure returns (Storage storage data) { + assembly { + data.slot := _WITNET_TRAPS_DATA_SLOTHASH + } + } +} From 926fd072041a11427e3f90dc0e2e8135a7b8bb1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Wed, 27 Dec 2023 15:12:20 +0100 Subject: [PATCH 7/7] chore: include WitnetTraps in migration scripts --- migrations/scripts/3_core.js | 21 +++++++++++++++++++-- migrations/scripts/4_proxies.js | 7 +++++++ migrations/witnet.settings.js | 11 +++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/migrations/scripts/3_core.js b/migrations/scripts/3_core.js index 887ae59a..eb20647c 100644 --- a/migrations/scripts/3_core.js +++ b/migrations/scripts/3_core.js @@ -83,6 +83,23 @@ module.exports = async function (_, network, [, from]) { utils.saveAddresses(addresses); } } + // Deploy/upgrade WitnetTraps target implementation, if required + { + await deploy({ + from, ecosystem, network, targets, + key: targets.WitnetTraps, + libs: specs.WitnetTraps.libs, + immutables: specs.WitnetTraps.immutables, + intrinsics: { types: [ 'address', 'bool', 'bytes32' ], values: [ + /* _registry */ await determineProxyAddr(from, specs.WitnetBytecodes?.vanity || 1), + /* _upgradable */ true, + /* _versionTag */ utils.fromAscii(version), + ]}, + }); + if (!isDryRun) { + utils.saveAddresses(addresses); + } + } } async function deploy(specs) { @@ -98,12 +115,12 @@ async function deploy(specs) { if (immutables?.values) values = [ ...values, ...immutables.values ] const constructorArgs = web3.eth.abi.encodeParameters(types, values) if (constructorArgs.length > 2) { - console.info(" ", "> constructor types:", types) + console.info(" ", "> constructor types:", JSON.stringify(types)) console.info(" ", "> constructor args: ", constructorArgs.slice(2)) } const coreBytecode = link(contract.toJSON().bytecode, libs, targets) if (coreBytecode.indexOf("__") > -1) { - console.info(bytecode) + console.info(coreBytecode) console.info("Error: Cannot deploy due to some missing libs") process.exit(1) } diff --git a/migrations/scripts/4_proxies.js b/migrations/scripts/4_proxies.js index e5fbcf5d..54f6cc82 100644 --- a/migrations/scripts/4_proxies.js +++ b/migrations/scripts/4_proxies.js @@ -30,6 +30,7 @@ module.exports = async function (_, network, [, from, reporter]) { "WitnetBytecodes", "WitnetRequestFactory", "WitnetRequestBoard", + "WitnetTraps", ] specs["WitnetRequestBoard"].mutables = merge({ @@ -38,6 +39,12 @@ module.exports = async function (_, network, [, from, reporter]) { }, specs["WitnetRequestBoard"].mutables ) + specs["WitnetTraps"].mutables = merge({ + types: [ 'address[]', ], + values: [ [ reporter, ], ], + }, specs["WitnetTraps"].mutables + ) + // Deploy/upgrade singleton proxies, if required for (index in singletons) { await deploy({ diff --git a/migrations/witnet.settings.js b/migrations/witnet.settings.js index 80bcc6fa..0ff2cd83 100644 --- a/migrations/witnet.settings.js +++ b/migrations/witnet.settings.js @@ -9,6 +9,7 @@ module.exports = { WitnetRandomness: "WitnetRandomness", WitnetRequestBoard: "WitnetRequestBoardTrustableDefault", WitnetRequestFactory: "WitnetRequestFactoryDefault", + WitnetTraps: "WitnetTrapsTrustableDefault", }, boba: { WitnetRequestBoard: "WitnetRequestBoardTrustableOvm2", @@ -684,6 +685,16 @@ module.exports = { WitnetPriceFeeds: { libs: [ "WitnetPriceFeedsLib", ], vanity: 5, + }, + WitnetTraps: { + immutables: { + types: [ 'uint256', 'uint256', ], + values: [ + /* _reportResultGasBase */ 58282, + /* _sstoreFromZeroGas */ 20000, + ], + }, + vanity: 7, } }, avalanche: {