diff --git a/package.json b/package.json index eb82655..bb2f368 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ "package.json": "sort-package-json" }, "dependencies": { - "@defi-wonderland/prophet-core-contracts": "0.0.0-438de1c5", - "@defi-wonderland/prophet-modules-contracts": "0.0.0-1197c328" + "@defi-wonderland/prophet-core": "0.0.0-2e39539b", + "@defi-wonderland/prophet-modules": "0.0.0-e52f8cce" }, "devDependencies": { "@commitlint/cli": "19.3.0", diff --git a/remappings.txt b/remappings.txt index 2245399..37f3fdd 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,7 +1,7 @@ forge-std/=node_modules/forge-std/src halmos-cheatcodes=node_modules/halmos-cheatcodes -@defi-wonderland/prophet-core-contracts/=node_modules/@defi-wonderland/prophet-core-contracts -@defi-wonderland/prophet-modules-contracts/=node_modules/@defi-wonderland/prophet-modules-contracts +@defi-wonderland/prophet-core/=node_modules/@defi-wonderland/prophet-core +@defi-wonderland/prophet-modules/=node_modules/@defi-wonderland/prophet-modules contracts/=src/contracts interfaces/=src/interfaces diff --git a/src/contracts/EBOFinalityModule.sol b/src/contracts/EBOFinalityModule.sol new file mode 100644 index 0000000..b420400 --- /dev/null +++ b/src/contracts/EBOFinalityModule.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Module} from '@defi-wonderland/prophet-core/solidity/contracts/Module.sol'; +import {IModule} from '@defi-wonderland/prophet-core/solidity/interfaces/IModule.sol'; +import {IOracle} from '@defi-wonderland/prophet-core/solidity/interfaces/IOracle.sol'; + +import {Arbitrable} from 'contracts/Arbitrable.sol'; +import {IEBOFinalityModule} from 'interfaces/IEBOFinalityModule.sol'; + +/** + * @title EBOFinalityModule + * @notice Module allowing users to index data into the subgraph + * as a result of a request being finalized + */ +contract EBOFinalityModule is Module, Arbitrable, IEBOFinalityModule { + /// @inheritdoc IEBOFinalityModule + address public eboRequestCreator; + + /** + * @notice Constructor + * @param _oracle The address of the Oracle + * @param _eboRequestCreator The address of the EBORequestCreator + * @param _arbitrator The address of The Graph's Arbitrator + * @param _council The address of The Graph's Council + */ + constructor( + IOracle _oracle, + address _eboRequestCreator, + address _arbitrator, + address _council + ) Module(_oracle) Arbitrable(_arbitrator, _council) { + _setEBORequestCreator(_eboRequestCreator); + } + + /// @inheritdoc IEBOFinalityModule + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external override(Module, IEBOFinalityModule) onlyOracle { + if (_request.requester != eboRequestCreator) revert EBOFinalityModule_InvalidRequester(); + + if (_response.requestId != 0) { + _validateResponse(_request, _response); + + // TODO: Redeclare the `Response` struct + // emit NewEpoch(_response.epoch, _response.chainId, _response.block); + } + + emit RequestFinalized(_response.requestId, _response, _finalizer); + } + + /// @inheritdoc IEBOFinalityModule + function amendEpoch( + uint256 _epoch, + uint256[] calldata _chainIds, + uint256[] calldata _blockNumbers + ) external onlyArbitrator { + uint256 _length = _chainIds.length; + if (_length != _blockNumbers.length) revert EBOFinalityModule_LengthMismatch(); + + for (uint256 _i; _i < _length; ++_i) { + emit AmendEpoch(_epoch, _chainIds[_i], _blockNumbers[_i]); + } + } + + /// @inheritdoc IEBOFinalityModule + function setEBORequestCreator(address _eboRequestCreator) external onlyArbitrator { + _setEBORequestCreator(_eboRequestCreator); + } + + /// @inheritdoc IModule + function moduleName() external pure returns (string memory _moduleName) { + _moduleName = 'EBOFinalityModule'; + } + + /** + * @notice Sets the address of the EBORequestCreator + * @param _eboRequestCreator The address of the EBORequestCreator + */ + function _setEBORequestCreator(address _eboRequestCreator) private { + eboRequestCreator = _eboRequestCreator; + emit SetEBORequestCreator(_eboRequestCreator); + } +} diff --git a/src/interfaces/IEBOFinalityModule.sol b/src/interfaces/IEBOFinalityModule.sol new file mode 100644 index 0000000..a247f44 --- /dev/null +++ b/src/interfaces/IEBOFinalityModule.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IOracle} from '@defi-wonderland/prophet-core/solidity/interfaces/IOracle.sol'; +import {IFinalityModule} from '@defi-wonderland/prophet-core/solidity/interfaces/modules/finality/IFinalityModule.sol'; + +import {IArbitrable} from 'interfaces/IArbitrable.sol'; + +/** + * @title EBOFinalityModule + * @notice Module allowing users to index data into the subgraph + * as a result of a request being finalized + */ +interface IEBOFinalityModule is IFinalityModule, IArbitrable { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Emitted when the block number has been resolved for a particular epoch-chainId pair + * @param _epoch The new epoch + * @param _chainId The chain ID + * @param _blockNumber The block number for the epoch-chainId pair + */ + event NewEpoch(uint256 _epoch, uint256 indexed _chainId, uint256 _blockNumber); + + /** + * @notice Emitted when a block number is amended + * @param _epoch The epoch to amend + * @param _chainId The chain ID to amend + * @param _blockNumber The amended block number + */ + event AmendEpoch(uint256 _epoch, uint256 indexed _chainId, uint256 _blockNumber); + + /** + * @notice Emitted when the EBORequestCreator is set + * @param _eboRequestCreator The address of the EBORequestCreator + */ + event SetEBORequestCreator(address _eboRequestCreator); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Thrown when the requester is not the EBORequestCreator + */ + error EBOFinalityModule_InvalidRequester(); + + /** + * @notice Thrown when the lengths of chain IDs and block numbers do not match + */ + error EBOFinalityModule_LengthMismatch(); + + /*/////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the address of the EBORequestCreator + * @return _eboRequestCreator The address of the EBORequestCreator + */ + function eboRequestCreator() external view returns (address _eboRequestCreator); + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Finalizes the request by publishing the response + * @dev Callable only by the Oracle + * @param _request The request being finalized + * @param _response The final response + * @param _finalizer The address that initiated the finalization + */ + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external; + + /** + * @notice Allows to amend data in case of an error or an emergency + * @dev Callable only by The Graph's Arbitrator + * @param _epoch The epoch to amend + * @param _chainIds The chain IDs to amend + * @param _blockNumbers The amended block numbers + */ + function amendEpoch(uint256 _epoch, uint256[] calldata _chainIds, uint256[] calldata _blockNumbers) external; + + /** + * @notice Sets the address of the EBORequestCreator + * @dev Callable only by The Graph's Arbitrator + * @param _eboRequestCreator The address of the EBORequestCreator + */ + function setEBORequestCreator(address _eboRequestCreator) external; +} diff --git a/test/unit/EBOFinalityModule.t.sol b/test/unit/EBOFinalityModule.t.sol new file mode 100644 index 0000000..b959fc4 --- /dev/null +++ b/test/unit/EBOFinalityModule.t.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IModule} from '@defi-wonderland/prophet-core/solidity/interfaces/IModule.sol'; +import {IOracle} from '@defi-wonderland/prophet-core/solidity/interfaces/IOracle.sol'; +import {IValidator} from '@defi-wonderland/prophet-core/solidity/interfaces/IValidator.sol'; +import {ValidatorLib} from '@defi-wonderland/prophet-core/solidity/libraries/ValidatorLib.sol'; + +import {IArbitrable} from 'interfaces/IArbitrable.sol'; +import {IEBOFinalityModule} from 'interfaces/IEBOFinalityModule.sol'; + +import {EBOFinalityModule} from 'contracts/EBOFinalityModule.sol'; + +import 'forge-std/Test.sol'; + +contract EBOFinalityModule_Unit_BaseTest is Test { + EBOFinalityModule public eboFinalityModule; + + IOracle public oracle; + address public eboRequestCreator; + address public arbitrator; + address public council; + + uint256 public constant FUZZED_ARRAY_LENGTH = 32; + + event NewEpoch(uint256 _epoch, uint256 indexed _chainId, uint256 _blockNumber); + event AmendEpoch(uint256 _epoch, uint256 indexed _chainId, uint256 _blockNumber); + event SetEBORequestCreator(address _eboRequestCreator); + event RequestFinalized(bytes32 indexed _requestId, IOracle.Response _response, address _finalizer); + event SetArbitrator(address _arbitrator); + event SetCouncil(address _council); + + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + eboRequestCreator = makeAddr('EBORequestCreator'); + arbitrator = makeAddr('Arbitrator'); + council = makeAddr('Council'); + + eboFinalityModule = new EBOFinalityModule(oracle, eboRequestCreator, arbitrator, council); + } + + function _getId(IOracle.Request memory _request) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_request)); + } + + function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_response)); + } + + function _getDynamicArray( + uint256[FUZZED_ARRAY_LENGTH] calldata _staticArray + ) internal pure returns (uint256[] memory _dynamicArray) { + _dynamicArray = new uint256[](FUZZED_ARRAY_LENGTH); + for (uint256 _i; _i < FUZZED_ARRAY_LENGTH; ++_i) { + _dynamicArray[_i] = _staticArray[_i]; + } + } +} + +contract EBOFinalityModule_Unit_Constructor is EBOFinalityModule_Unit_BaseTest { + struct ConstructorParams { + IOracle oracle; + address eboRequestCreator; + address arbitrator; + address council; + } + + function test_setOracle(ConstructorParams calldata _params) public { + eboFinalityModule = + new EBOFinalityModule(_params.oracle, _params.eboRequestCreator, _params.arbitrator, _params.council); + + assertEq(address(eboFinalityModule.ORACLE()), address(_params.oracle)); + } + + function test_setArbitrator(ConstructorParams calldata _params) public { + eboFinalityModule = + new EBOFinalityModule(_params.oracle, _params.eboRequestCreator, _params.arbitrator, _params.council); + + assertEq(eboFinalityModule.arbitrator(), _params.arbitrator); + } + + function test_emitSetArbitrator(ConstructorParams calldata _params) public { + vm.expectEmit(); + emit SetArbitrator(_params.arbitrator); + new EBOFinalityModule(_params.oracle, _params.eboRequestCreator, _params.arbitrator, _params.council); + } + + function test_setCouncil(ConstructorParams calldata _params) public { + eboFinalityModule = + new EBOFinalityModule(_params.oracle, _params.eboRequestCreator, _params.arbitrator, _params.council); + + assertEq(eboFinalityModule.council(), _params.council); + } + + function test_emitSetCouncil(ConstructorParams calldata _params) public { + vm.expectEmit(); + emit SetCouncil(_params.council); + new EBOFinalityModule(_params.oracle, _params.eboRequestCreator, _params.arbitrator, _params.council); + } + + function test_setEBORequestCreator(ConstructorParams calldata _params) public { + eboFinalityModule = + new EBOFinalityModule(_params.oracle, _params.eboRequestCreator, _params.arbitrator, _params.council); + + assertEq(eboFinalityModule.eboRequestCreator(), _params.eboRequestCreator); + } + + function test_emitSetEBORequestCreator(ConstructorParams calldata _params) public { + vm.expectEmit(); + emit SetEBORequestCreator(_params.eboRequestCreator); + new EBOFinalityModule(_params.oracle, _params.eboRequestCreator, _params.arbitrator, _params.council); + } +} + +contract EBOFinalityModule_Unit_FinalizeRequest is EBOFinalityModule_Unit_BaseTest { + struct FinalizeRequestParams { + IOracle.Request request; + IOracle.Response response; + address finalizer; + uint128 responseCreatedAt; + bool finalizeWithResponse; + } + + modifier happyPath(FinalizeRequestParams memory _params) { + _params.request.requester = eboRequestCreator; + + if (_params.finalizeWithResponse) { + _params.response.requestId = _getId(_params.request); + + vm.assume(_params.responseCreatedAt != 0); + vm.mockCall( + address(oracle), + abi.encodeCall(IOracle.responseCreatedAt, (_getId(_params.response))), + abi.encode(_params.responseCreatedAt) + ); + } else { + _params.response.requestId = 0; + } + + vm.startPrank(address(oracle)); + _; + } + + function test_revertOnlyOracle(FinalizeRequestParams memory _params) public happyPath(_params) { + vm.stopPrank(); + vm.expectRevert(IModule.Module_OnlyOracle.selector); + eboFinalityModule.finalizeRequest(_params.request, _params.response, _params.finalizer); + } + + function test_revertInvalidRequester( + FinalizeRequestParams memory _params, + address _requester + ) public happyPath(_params) { + vm.assume(_requester != eboRequestCreator); + _params.request.requester = _requester; + + vm.expectRevert(IEBOFinalityModule.EBOFinalityModule_InvalidRequester.selector); + eboFinalityModule.finalizeRequest(_params.request, _params.response, _params.finalizer); + } + + function test_revertInvalidResponseBody( + FinalizeRequestParams memory _params, + bytes32 _requestId + ) public happyPath(_params) { + vm.assume(_params.finalizeWithResponse); + vm.assume(_requestId != 0); + vm.assume(_requestId != _getId(_params.request)); + _params.response.requestId = _requestId; + + vm.expectRevert(ValidatorLib.ValidatorLib_InvalidResponseBody.selector); + eboFinalityModule.finalizeRequest(_params.request, _params.response, _params.finalizer); + } + + function test_revertInvalidResponse(FinalizeRequestParams memory _params) public happyPath(_params) { + vm.assume(_params.finalizeWithResponse); + vm.mockCall(address(oracle), abi.encodeCall(IOracle.responseCreatedAt, (_getId(_params.response))), abi.encode(0)); + + vm.expectRevert(IValidator.Validator_InvalidResponse.selector); + eboFinalityModule.finalizeRequest(_params.request, _params.response, _params.finalizer); + } + + function test_emitNewEpoch(FinalizeRequestParams memory _params) public happyPath(_params) { + vm.assume(_params.finalizeWithResponse); + + vm.skip(true); + // vm.expectEmit(); + // emit NewEpoch(_params.response.epoch, _params.response.chainId, _params.response.block); + eboFinalityModule.finalizeRequest(_params.request, _params.response, _params.finalizer); + } + + function test_emitRequestFinalized(FinalizeRequestParams memory _params) public happyPath(_params) { + vm.expectEmit(); + emit RequestFinalized(_params.response.requestId, _params.response, _params.finalizer); + eboFinalityModule.finalizeRequest(_params.request, _params.response, _params.finalizer); + } +} + +contract EBOFinalityModule_Unit_AmendEpoch is EBOFinalityModule_Unit_BaseTest { + struct AmendEpochParams { + uint256 epoch; + uint256[FUZZED_ARRAY_LENGTH] chainIds; + uint256[FUZZED_ARRAY_LENGTH] blockNumbers; + } + + modifier happyPath() { + vm.startPrank(arbitrator); + _; + } + + function test_revertOnlyArbitrator(AmendEpochParams calldata _params) public happyPath { + uint256[] memory _chainIds = _getDynamicArray(_params.chainIds); + uint256[] memory _blockNumbers = _getDynamicArray(_params.blockNumbers); + + vm.stopPrank(); + vm.expectRevert(IArbitrable.Arbitrable_OnlyArbitrator.selector); + eboFinalityModule.amendEpoch(_params.epoch, _chainIds, _blockNumbers); + } + + function test_revertLengthMismatch( + AmendEpochParams calldata _params, + uint256[] calldata _chainIds, + uint256[] calldata _blockNumbers + ) public happyPath { + vm.assume(_chainIds.length != _blockNumbers.length); + + vm.expectRevert(IEBOFinalityModule.EBOFinalityModule_LengthMismatch.selector); + eboFinalityModule.amendEpoch(_params.epoch, _chainIds, _blockNumbers); + } + + function test_emitAmendEpoch(AmendEpochParams calldata _params) public happyPath { + uint256[] memory _chainIds = _getDynamicArray(_params.chainIds); + uint256[] memory _blockNumbers = _getDynamicArray(_params.blockNumbers); + + for (uint256 _i; _i < _chainIds.length; ++_i) { + vm.expectEmit(); + emit AmendEpoch(_params.epoch, _chainIds[_i], _blockNumbers[_i]); + } + eboFinalityModule.amendEpoch(_params.epoch, _chainIds, _blockNumbers); + } +} + +contract EBOFinalityModule_Unit_SetEBORequestCreator is EBOFinalityModule_Unit_BaseTest { + modifier happyPath() { + vm.startPrank(arbitrator); + _; + } + + function test_revertOnlyArbitrator(address _eboRequestCreator) public happyPath { + vm.stopPrank(); + vm.expectRevert(IArbitrable.Arbitrable_OnlyArbitrator.selector); + eboFinalityModule.setEBORequestCreator(_eboRequestCreator); + } + + function test_setEBORequestCreator(address _eboRequestCreator) public happyPath { + eboFinalityModule.setEBORequestCreator(_eboRequestCreator); + + assertEq(eboFinalityModule.eboRequestCreator(), _eboRequestCreator); + } + + function test_emitSetEBORequestCreator(address _eboRequestCreator) public happyPath { + vm.expectEmit(); + emit SetEBORequestCreator(_eboRequestCreator); + eboFinalityModule.setEBORequestCreator(_eboRequestCreator); + } +} + +contract EBOFinalityModule_Unit_ModuleName is EBOFinalityModule_Unit_BaseTest { + function test_returnModuleName() public view { + assertEq(eboFinalityModule.moduleName(), 'EBOFinalityModule'); + } +} diff --git a/yarn.lock b/yarn.lock index f10f820..3793e60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -190,22 +190,17 @@ solc-typed-ast "18.1.2" yargs "17.7.2" -"@defi-wonderland/prophet-core-contracts@0.0.0-438de1c5": - version "0.0.0-438de1c5" - resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-438de1c5.tgz#2b0238636c89eb71b07d57e12abfdea57c53b03b" - integrity sha512-UqdNRr2eH5DNSQ7qUriZbvR9LoXLXDMKTOjcjTTc8Gbq1aVcBhwFu4mSdrnhpQkXHjKtmsO0IfGcRHIGLvnXSQ== - -"@defi-wonderland/prophet-core-contracts@0.0.0-d01bc1a0": - version "0.0.0-d01bc1a0" - resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-d01bc1a0.tgz#ee4e8d970289a26966f6565b2f691d68d7b4232a" - integrity sha512-n4Dl1QgQAZafOtV7ef/fSoew2qlxWSdS389Z6PdIn7LxrHrzzRJi+nmJ1DJazMvMuhk/0dZbMhnaXMBi05E1zQ== - -"@defi-wonderland/prophet-modules-contracts@0.0.0-1197c328": - version "0.0.0-1197c328" - resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-modules-contracts/-/prophet-modules-contracts-0.0.0-1197c328.tgz#f58aa52a5a33fb39a9404bdf5e16ab75fcdde85f" - integrity sha512-78IRF5dSoHvVIzS9/NSqkuQrzPLrVKzRnBvOeSWt/vzoKy3Pm6rkcT5qzWZynIXbFNyMXGTa8HKYBWKFNlaE9A== - dependencies: - "@defi-wonderland/prophet-core-contracts" "0.0.0-d01bc1a0" +"@defi-wonderland/prophet-core@0.0.0-2e39539b": + version "0.0.0-2e39539b" + resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core/-/prophet-core-0.0.0-2e39539b.tgz#dbf01ed05a9af302841123c77e84bc0b3b4a6176" + integrity sha512-EdYpDEO1XeO08uQikhOQ6NzG0LWYxANFk272j4vCyLSJ8kRyJNMv69JJCLcq5kV0B9IzXybmqjreemkZ05z3kQ== + +"@defi-wonderland/prophet-modules@0.0.0-e52f8cce": + version "0.0.0-e52f8cce" + resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-modules/-/prophet-modules-0.0.0-e52f8cce.tgz#5b0cee1c75d1127267ccee29cdcdb2b3a2bfe5dd" + integrity sha512-zNx602Z/GuisTqi120JLgoWcs3wiMKDYvnXJ/6xdYUCNNBQKk2GyCmS6M5Hu1vTbSfDlNRrugy1cDOkG5sSGdg== + dependencies: + "@defi-wonderland/prophet-core" "0.0.0-2e39539b" "@openzeppelin/contracts" "4.9.5" solmate "https://github.com/transmissions11/solmate.git#bfc9c25865a274a7827fea5abf6e4fb64fc64e6c"