diff --git a/docs/src/content/modules/dispute/root_verification_module.md b/docs/src/content/modules/dispute/root_verification_module.md index 6bb6e1f0..b2ad1935 100644 --- a/docs/src/content/modules/dispute/root_verification_module.md +++ b/docs/src/content/modules/dispute/root_verification_module.md @@ -10,10 +10,9 @@ The Root Verification Module is a pre-dispute module that allows disputers to ca ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: Returns the decoded data for a request. -- `disputeResponse(bytes32 _requestId, bytes32 _responseId, address _disputer, address _proposer)`: Calculates the correct root and compares it to the proposed one. Updates the dispute status after checking if the disputed response is indeed wrong. -- `onDisputeStatusChange(bytes32 _requestId, IOracle.Dispute memory _dispute)`: Updates the status of the dispute and resolves it by proposing the correct root as a response and finalizing the request. -- `disputeEscalated(bytes32 _disputeId)`: This function is present to comply with the module interface but it is not implemented since this is a pre-dispute module. +- `decodeRequestData`: Returns the decoded data for a request. +- `disputeResponse`: Calculates the correct root and compares it to the proposed one. Updates the dispute status after checking if the disputed response is indeed wrong. +- `onDisputeStatusChange`: Updates the status of the dispute and resolves it by proposing the correct root as a response and finalizing the request. ### Request Parameters diff --git a/solidity/contracts/modules/dispute/RootVerificationModule.sol b/solidity/contracts/modules/dispute/RootVerificationModule.sol index e19f1cf0..e954fc1a 100644 --- a/solidity/contracts/modules/dispute/RootVerificationModule.sol +++ b/solidity/contracts/modules/dispute/RootVerificationModule.sol @@ -1,94 +1,95 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// // solhint-disable-next-line no-unused-import -// import {Module, IModule} from '@defi-wonderland/prophet-core-contracts/solidity/contracts/Module.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; - -// import {IRootVerificationModule} from '../../../interfaces/modules/dispute/IRootVerificationModule.sol'; -// import {MerkleLib} from '../../libraries/MerkleLib.sol'; - -// contract RootVerificationModule is Module, IRootVerificationModule { -// using MerkleLib for MerkleLib.Tree; - -// /** -// * @notice The calculated correct root for a given request -// */ -// mapping(bytes32 _requestId => bytes32 _correctRoot) internal _correctRoots; - -// constructor(IOracle _oracle) Module(_oracle) {} - -// /// @inheritdoc IModule -// function moduleName() external pure returns (string memory _moduleName) { -// return 'RootVerificationModule'; -// } - -// /// @inheritdoc IRootVerificationModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } - -// /// @inheritdoc IRootVerificationModule -// function disputeEscalated(bytes32 _disputeId) external onlyOracle {} - -// /// @inheritdoc IRootVerificationModule -// function onDisputeStatusChange(bytes32, IOracle.Dispute memory _dispute) external onlyOracle { -// RequestParameters memory _params = decodeRequestData(_dispute.requestId); - -// IOracle.Response memory _response = ORACLE.getResponse(_dispute.responseId); - -// bool _won = abi.decode(_response.response, (bytes32)) != _correctRoots[_dispute.requestId]; - -// if (_won) { -// _params.accountingExtension.pay({ -// _requestId: _dispute.requestId, -// _payer: _dispute.proposer, -// _receiver: _dispute.disputer, -// _token: _params.bondToken, -// _amount: _params.bondSize -// }); -// bytes32 _correctResponseId = -// ORACLE.proposeResponse(_dispute.disputer, _dispute.requestId, abi.encode(_correctRoots[_dispute.requestId])); -// ORACLE.finalize(_dispute.requestId, _correctResponseId); -// } else { -// ORACLE.finalize(_dispute.requestId, _dispute.responseId); -// } - -// delete _correctRoots[_dispute.requestId]; - -// emit DisputeStatusChanged({ -// _requestId: _dispute.requestId, -// _responseId: _dispute.responseId, -// _disputer: _dispute.disputer, -// _proposer: _dispute.proposer, -// _status: _dispute.status -// }); -// } - -// /// @inheritdoc IRootVerificationModule -// function disputeResponse( -// bytes32 _requestId, -// bytes32 _responseId, -// address _disputer, -// address _proposer -// ) external onlyOracle returns (IOracle.Dispute memory _dispute) { -// IOracle.Response memory _response = ORACLE.getResponse(_responseId); -// RequestParameters memory _params = decodeRequestData(_requestId); - -// bytes32 _correctRoot = _params.treeVerifier.calculateRoot(_params.treeData, _params.leavesToInsert); -// _correctRoots[_requestId] = _correctRoot; - -// bool _won = abi.decode(_response.response, (bytes32)) != _correctRoot; - -// _dispute = IOracle.Dispute({ -// disputer: _disputer, -// responseId: _responseId, -// proposer: _proposer, -// requestId: _requestId, -// status: _won ? IOracle.DisputeStatus.Won : IOracle.DisputeStatus.Lost, -// createdAt: block.timestamp -// }); - -// emit ResponseDisputed(_requestId, _responseId, _disputer, _proposer); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// solhint-disable-next-line no-unused-import +import {Module, IModule} from '@defi-wonderland/prophet-core-contracts/solidity/contracts/Module.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; + +import {IRootVerificationModule} from '../../../interfaces/modules/dispute/IRootVerificationModule.sol'; +import {MerkleLib} from '../../libraries/MerkleLib.sol'; + +contract RootVerificationModule is Module, IRootVerificationModule { + using MerkleLib for MerkleLib.Tree; + + /** + * @notice The calculated correct root for a given request + */ + mapping(bytes32 _requestId => bytes32 _correctRoot) internal _correctRoots; + + constructor(IOracle _oracle) Module(_oracle) {} + + /// @inheritdoc IModule + function moduleName() external pure returns (string memory _moduleName) { + return 'RootVerificationModule'; + } + + /// @inheritdoc IRootVerificationModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } + + /// @inheritdoc IRootVerificationModule + function onDisputeStatusChange( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + // TODO: Call `disputeStatus` to check the current status + RequestParameters memory _params = decodeRequestData(_request.disputeModuleData); + + bytes32 _correctRoot = _correctRoots[_dispute.requestId]; + bool _won = abi.decode(_response.response, (bytes32)) != _correctRoot; + + if (_won) { + _params.accountingExtension.pay({ + _requestId: _dispute.requestId, + _payer: _dispute.proposer, + _receiver: _dispute.disputer, + _token: _params.bondToken, + _amount: _params.bondSize + }); + + IOracle.Response memory _newResponse = IOracle.Response({ + requestId: _dispute.requestId, + proposer: _dispute.disputer, + response: abi.encode(_correctRoot) + }); + + emit DisputeStatusChanged({_disputeId: _disputeId, _dispute: _dispute, _status: IOracle.DisputeStatus.Won}); + + ORACLE.proposeResponse(_request, _newResponse); + ORACLE.finalize(_request, _newResponse); + } else { + emit DisputeStatusChanged({_disputeId: _disputeId, _dispute: _dispute, _status: IOracle.DisputeStatus.Lost}); + ORACLE.finalize(_request, _response); + } + + delete _correctRoots[_dispute.requestId]; + } + + /// @inheritdoc IRootVerificationModule + function disputeResponse( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.disputeModuleData); + + bytes32 _correctRoot = _params.treeVerifier.calculateRoot(_params.treeData, _params.leavesToInsert); + _correctRoots[_response.requestId] = _correctRoot; + + IOracle.DisputeStatus _status = + abi.decode(_response.response, (bytes32)) != _correctRoot ? IOracle.DisputeStatus.Won : IOracle.DisputeStatus.Lost; + + emit ResponseDisputed({ + _requestId: _response.requestId, + _responseId: _dispute.responseId, + _disputeId: _getId(_dispute), + _dispute: _dispute, + _blockNumber: block.number + }); + + ORACLE.updateDisputeStatus(_request, _response, _dispute, _status); + } +} diff --git a/solidity/interfaces/modules/dispute/IRootVerificationModule.sol b/solidity/interfaces/modules/dispute/IRootVerificationModule.sol index 682468eb..f29c92f1 100644 --- a/solidity/interfaces/modules/dispute/IRootVerificationModule.sol +++ b/solidity/interfaces/modules/dispute/IRootVerificationModule.sol @@ -1,84 +1,81 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; -// import {IDisputeModule} from -// '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/dispute/IDisputeModule.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IDisputeModule} from + '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/dispute/IDisputeModule.sol'; -// import {ITreeVerifier} from '../../ITreeVerifier.sol'; -// import {IAccountingExtension} from '../../extensions/IAccountingExtension.sol'; +import {ITreeVerifier} from '../../ITreeVerifier.sol'; +import {IAccountingExtension} from '../../extensions/IAccountingExtension.sol'; -// /* -// * @title RootVerificationModule -// * @notice Dispute module allowing disputers to calculate the correct root -// * for a given request and propose it as a response. If the disputer wins the -// * dispute, he is rewarded with the bond of the proposer. -// * @dev This module is a pre-dispute module. It allows disputing -// * and resolving a response in a single call. -// */ -// interface IRootVerificationModule is IDisputeModule { -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ +/* + * @title RootVerificationModule + * @notice Dispute module allowing disputers to calculate the correct root for a given request and propose it as a response. + * If the disputer wins the dispute, he is rewarded with the bond of the proposer. + * + * @dev This module is a pre-dispute module. It allows disputing and resolving a response in a single call. + */ +interface IRootVerificationModule is IDisputeModule { + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Parameters of the request as stored in the module -// * @param treeData The data of the tree -// * @param leavesToInsert The leaves to insert in the tree -// * @param treeVerifier The tree verifier to use to calculate the correct root -// * @param accountingExtension The accounting extension to use for bonds and payments -// * @param bondToken The token to use for bonds and payments -// * @param bondSize The size of the bond to participate in the request -// */ -// struct RequestParameters { -// bytes treeData; -// bytes32[] leavesToInsert; -// ITreeVerifier treeVerifier; -// IAccountingExtension accountingExtension; -// IERC20 bondToken; -// uint256 bondSize; -// } -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ + /** + * @notice Parameters of the request as stored in the module + * @param treeData The data of the tree + * @param leavesToInsert The leaves to insert in the tree + * @param treeVerifier The tree verifier to use to calculate the correct root + * @param accountingExtension The accounting extension to use for bonds and payments + * @param bondToken The token to use for bonds and payments + * @param bondSize The size of the bond to participate in the request + */ + struct RequestParameters { + bytes treeData; + bytes32[] leavesToInsert; + ITreeVerifier treeVerifier; + IAccountingExtension accountingExtension; + IERC20 bondToken; + uint256 bondSize; + } + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Returns the decoded data for a request -// * @param _requestId The ID of the request -// * @return _params The struct containing the parameters for the request -// */ -// function decodeRequestData(bytes32 _requestId) external view returns (RequestParameters memory _params); + /** + * @notice Returns the decoded data for a request + * @param _data The encoded request parameters + * @return _params The decoded parameters of the request + */ + function decodeRequestData(bytes calldata _data) external view returns (RequestParameters memory _params); -// /** -// * @notice Calculates the correct root and compares it to the proposed one. -// * @dev Since this is a pre-dispute module, the dispute status is updated after checking -// * if the disputed response is indeed wrong, since it is calculated on dispute. -// * @param _requestId The id of the request from which the response is being disputed -// * @param _responseId The id of the response being disputed -// * @param _disputer The user who is disputing the response -// * @param _proposer The proposer of the response being disputed -// * @return _dispute The dispute of the current response with the updated status -// */ -// function disputeResponse( -// bytes32 _requestId, -// bytes32 _responseId, -// address _disputer, -// address _proposer -// ) external returns (IOracle.Dispute memory _dispute); + /** + * @notice Initiates and resolves the dispute by comparing the proposed response with the one returned by the verifier + * + * @dev This function will notify the oracle about the outcome of the dispute + * @param _request The request that the response was proposed to + * @param _response The response that is being disputed + * @param _dispute The dispute created by the oracle + */ + function disputeResponse( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; -// /** -// * @notice Updates the status of the dispute and resolves it by proposing the correct root -// * as a response and finalizing the request. -// * @dev The correct root is retrieved from storage and compared to the proposed root. -// * If the dispute is won, the disputer is paid. In both cases, the request is finalized. -// * @param _dispute The dispute of the current response -// */ -// function onDisputeStatusChange(bytes32, IOracle.Dispute memory _dispute) external; - -// /** -// * @dev This function is present to comply with the module interface but it -// * is not implemented since this is a pre-dispute module. -// */ -// function disputeEscalated(bytes32 _disputeId) external; -// } + /** + * @notice Depending on the status of the dispute, either pays the disputer and submits the correct response, + * or pays the proposer. Finalizes the request in any case. + * + * @param _disputeId The id of the dispute + * @param _request The request that the response was proposed to + * @param _response The response that was disputed + * @param _dispute The dispute + */ + function onDisputeStatusChange( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; +} diff --git a/solidity/test/unit/modules/dispute/RootVerificationModule.t.sol b/solidity/test/unit/modules/dispute/RootVerificationModule.t.sol index 21ec848d..7959bd10 100644 --- a/solidity/test/unit/modules/dispute/RootVerificationModule.t.sol +++ b/solidity/test/unit/modules/dispute/RootVerificationModule.t.sol @@ -1,356 +1,304 @@ -// // SPDX-License-Identifier: AGPL-3.0-only -// pragma solidity ^0.8.19; - -// import 'forge-std/Test.sol'; - -// import {Helpers} from '../../../utils/Helpers.sol'; - -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; -// import {IModule} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IModule.sol'; - -// import { -// RootVerificationModule, -// IRootVerificationModule -// } from '../../../../contracts/modules/dispute/RootVerificationModule.sol'; - -// import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; -// import {ITreeVerifier} from '../../../../interfaces/ITreeVerifier.sol'; - -// /** -// * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks -// */ -// contract ForTest_RootVerificationModule is RootVerificationModule { -// constructor(IOracle _oracle) RootVerificationModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } -// } - -// /** -// * @title Root Verification Module Unit tests -// */ -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_RootVerificationModule public rootVerificationModule; -// // A mock oracle -// IOracle public oracle; -// // A mock accounting extension -// IAccountingExtension public accountingExtension; -// // Some unnoticeable dude -// address public dude = makeAddr('dude'); -// // 100% random sequence of bytes representing request, response, or dispute id -// bytes32 public mockId = bytes32('69'); -// // Create a new dummy dispute -// IOracle.Dispute public mockDispute; -// // A mock tree verifier -// ITreeVerifier public treeVerifier; -// // Mock addresses -// IERC20 public _token = IERC20(makeAddr('token')); -// address public _disputer = makeAddr('disputer'); -// address public _proposer = makeAddr('proposer'); - -// // Mock request data -// bytes32[32] internal _treeBranches = [ -// bytes32('branch1'), -// bytes32('branch2'), -// bytes32('branch3'), -// bytes32('branch4'), -// bytes32('branch5'), -// bytes32('branch6'), -// bytes32('branch7'), -// bytes32('branch8'), -// bytes32('branch9'), -// bytes32('branch10'), -// bytes32('branch11'), -// bytes32('branch12'), -// bytes32('branch13'), -// bytes32('branch14'), -// bytes32('branch15'), -// bytes32('branch16'), -// bytes32('branch17'), -// bytes32('branch18'), -// bytes32('branch19'), -// bytes32('branch20'), -// bytes32('branch21'), -// bytes32('branch22'), -// bytes32('branch23'), -// bytes32('branch24'), -// bytes32('branch25'), -// bytes32('branch26'), -// bytes32('branch27'), -// bytes32('branch28'), -// bytes32('branch29'), -// bytes32('branch30'), -// bytes32('branch31'), -// bytes32('branch32') -// ]; -// uint256 internal _treeCount = 1; -// bytes internal _treeData = abi.encode(_treeBranches, _treeCount); -// bytes32[] internal _leavesToInsert = [bytes32('leave1'), bytes32('leave2')]; - -// event ResponseDisputed(bytes32 indexed _requestId, bytes32 _responseId, address _disputer, address _proposer); - -// /** -// * @notice Deploy the target and mock oracle+accounting extension -// */ -// function setUp() public { -// oracle = IOracle(makeAddr('Oracle')); -// vm.etch(address(oracle), hex'069420'); - -// accountingExtension = IAccountingExtension(makeAddr('AccountingExtension')); -// vm.etch(address(accountingExtension), hex'069420'); -// treeVerifier = ITreeVerifier(makeAddr('TreeVerifier')); -// vm.etch(address(treeVerifier), hex'069420'); - -// rootVerificationModule = new ForTest_RootVerificationModule(oracle); - -// mockDispute = IOracle.Dispute({ -// createdAt: block.timestamp, -// disputer: dude, -// responseId: mockId, -// proposer: dude, -// requestId: mockId, -// status: IOracle.DisputeStatus.Active -// }); -// } -// } - -// contract RootVerificationModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(rootVerificationModule.moduleName(), 'RootVerificationModule'); -// } - -// /** -// * @notice Test that the decodeRequestData function returns the correct values -// */ -// function test_decodeRequestData_returnsCorrectData( -// bytes32 _requestId, -// address _accountingExtension, -// address _randomToken, -// uint256 _bondSize -// ) public { -// // Mock data -// bytes memory _requestData = abi.encode( -// IRootVerificationModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: treeVerifier, -// accountingExtension: IAccountingExtension(_accountingExtension), -// bondToken: IERC20(_randomToken), -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// rootVerificationModule.forTest_setRequestData(_requestId, _requestData); - -// IRootVerificationModule.RequestParameters memory _params = rootVerificationModule.decodeRequestData(_requestId); - -// bytes32[32] memory _treeBranchesStored; -// uint256 _treeCountStored; -// (_treeBranchesStored, _treeCountStored) = abi.decode(_params.treeData, (bytes32[32], uint256)); - -// // Check: is the request data properly stored? -// for (uint256 _i = 0; _i < _treeBranches.length; _i++) { -// assertEq(_treeBranchesStored[_i], _treeBranches[_i], 'Mismatch: decoded tree branch'); -// } -// for (uint256 _i = 0; _i < _leavesToInsert.length; _i++) { -// assertEq(_params.leavesToInsert[_i], _leavesToInsert[_i], 'Mismatch: decoded leave to insert'); -// } -// assertEq(_treeCountStored, _treeCount, 'Mismatch: decoded tree count'); -// assertEq(address(_params.treeVerifier), address(treeVerifier), 'Mismatch: decoded tree verifier'); -// assertEq(address(_params.accountingExtension), _accountingExtension, 'Mismatch: decoded accounting extension'); -// assertEq(address(_params.bondToken), _randomToken, 'Mismatch: decoded token'); -// assertEq(_params.bondSize, _bondSize, 'Mismatch: decoded bond size'); -// } -// } - -// contract RootVerificationModule_Unit_DisputeResponse is BaseTest { -// /** -// * @notice Test if dispute incorrect response returns the correct status -// */ -// function test_disputeIncorrectResponse(bytes32 _requestId, bytes32 _responseId, uint256 _bondSize) public { -// vm.assume(_requestId != _responseId); - -// // Mock request data -// bytes memory _requestData = abi.encode( -// IRootVerificationModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: treeVerifier, -// accountingExtension: accountingExtension, -// bondToken: _token, -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// rootVerificationModule.forTest_setRequestData(_requestId, _requestData); - -// // Create new Response memory struct with random values -// IOracle.Response memory _mockResponse = IOracle.Response({ -// createdAt: block.timestamp, -// proposer: _proposer, -// requestId: _requestId, -// disputeId: mockId, -// response: abi.encode(bytes32('randomRoot')) -// }); - -// // Mock and expect the call to the oracle, getting the response -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getResponse, (_responseId)), abi.encode(_mockResponse)); - -// // Mock and expect the call to the tree verifier, calculating the root -// _mockAndExpect( -// address(treeVerifier), -// abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), -// abi.encode(bytes32('randomRoot2')) -// ); - -// vm.prank(address(oracle)); -// IOracle.Dispute memory _dispute = -// rootVerificationModule.disputeResponse(_requestId, _responseId, _disputer, _proposer); - -// // Check: is the dispute data properly stored? -// assertEq(_dispute.disputer, _disputer, 'Mismatch: disputer'); -// assertEq(_dispute.proposer, _proposer, 'Mismatch: proposer'); -// assertEq(_dispute.responseId, _responseId, 'Mismatch: responseId'); -// assertEq(_dispute.requestId, _requestId, 'Mismatch: requestId'); -// assertEq(uint256(_dispute.status), uint256(IOracle.DisputeStatus.Won), 'Mismatch: status'); -// assertEq(_dispute.createdAt, block.timestamp, 'Mismatch: createdAt'); -// } - -// function test_emitsEvent(bytes32 _requestId, bytes32 _responseId, uint256 _bondSize) public { -// vm.assume(_requestId != _responseId); - -// // Mock request data -// bytes memory _requestData = abi.encode( -// IRootVerificationModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: treeVerifier, -// accountingExtension: accountingExtension, -// bondToken: _token, -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// rootVerificationModule.forTest_setRequestData(_requestId, _requestData); - -// // Create new Response memory struct with random values -// IOracle.Response memory _mockResponse = IOracle.Response({ -// createdAt: block.timestamp, -// proposer: _proposer, -// requestId: _requestId, -// disputeId: mockId, -// response: abi.encode(bytes32('randomRoot')) -// }); - -// // Mock and expect the call to the oracle, getting the response -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getResponse, (_responseId)), abi.encode(_mockResponse)); - -// // Mock and expect the call to the tree verifier, calculating the root -// _mockAndExpect( -// address(treeVerifier), -// abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), -// abi.encode(bytes32('randomRoot2')) -// ); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(rootVerificationModule)); -// emit ResponseDisputed(_requestId, _responseId, _disputer, _proposer); - -// vm.prank(address(oracle)); -// rootVerificationModule.disputeResponse(_requestId, _responseId, _disputer, _proposer); -// } - -// /** -// * @notice Test if dispute correct response returns the correct status -// */ -// function test_disputeCorrectResponse(bytes32 _requestId, bytes32 _responseId, uint256 _bondSize) public { -// vm.assume(_requestId != _responseId); - -// bytes memory _encodedCorrectRoot = abi.encode(bytes32('randomRoot')); - -// // Mock request data -// bytes memory _requestData = abi.encode( -// IRootVerificationModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: treeVerifier, -// accountingExtension: accountingExtension, -// bondToken: _token, -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// rootVerificationModule.forTest_setRequestData(_requestId, _requestData); - -// // Create new Response memory struct with random values -// IOracle.Response memory _mockResponse = IOracle.Response({ -// createdAt: block.timestamp, -// proposer: _proposer, -// requestId: _requestId, -// disputeId: mockId, -// response: _encodedCorrectRoot -// }); - -// // Mock and expect the call to the oracle, getting the response -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getResponse, (_responseId)), abi.encode(_mockResponse)); - -// // Mock and expect the call to the tree verifier, calculating the root -// _mockAndExpect( -// address(treeVerifier), -// abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), -// _encodedCorrectRoot -// ); - -// vm.prank(address(oracle)); -// IOracle.Dispute memory _dispute = -// rootVerificationModule.disputeResponse(_requestId, _responseId, _disputer, _proposer); - -// // Check: is the dispute data properly stored? -// assertEq(_dispute.disputer, _disputer, 'Mismatch: disputer'); -// assertEq(_dispute.proposer, _proposer, 'Mismatch: proposer'); -// assertEq(_dispute.responseId, _responseId, 'Mismatch: responseId'); -// assertEq(_dispute.requestId, _requestId, 'Mismatch: requestId'); -// assertEq(uint256(_dispute.status), uint256(IOracle.DisputeStatus.Lost), 'Mismatch: status'); -// assertEq(_dispute.createdAt, block.timestamp, 'Mismatch: createdAt'); -// } - -// /** -// * @notice Test if dispute response reverts when called by caller who's not the oracle -// */ -// function test_revertWrongCaller(address _randomCaller) public { -// vm.assume(_randomCaller != address(oracle)); - -// // Check: revert if not called by the Oracle? -// vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); - -// vm.prank(_randomCaller); -// rootVerificationModule.disputeResponse(mockId, mockId, dude, dude); -// } -// } - -// contract RootVerificationModule_Unit_DisputeEscalated is BaseTest { -// /** -// * @notice Test if dispute escalated do nothing -// */ -// function test_returnCorrectStatus() public { -// // Record sstore and sload -// vm.prank(address(oracle)); -// vm.record(); -// rootVerificationModule.disputeEscalated(mockId); -// (bytes32[] memory _reads, bytes32[] memory _writes) = vm.accesses(address(rootVerificationModule)); - -// // Check: no storage access? -// assertEq(_reads.length, 0); -// assertEq(_writes.length, 0); -// } -// } +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; + +import {Helpers} from '../../../utils/Helpers.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IModule} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IModule.sol'; + +import { + RootVerificationModule, + IRootVerificationModule +} from '../../../../contracts/modules/dispute/RootVerificationModule.sol'; + +import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; +import {ITreeVerifier} from '../../../../interfaces/ITreeVerifier.sol'; + +/** + * @title Root Verification Module Unit tests + */ +contract BaseTest is Test, Helpers { + // The target contract + RootVerificationModule public rootVerificationModule; + // A mock oracle + IOracle public oracle; + // A mock accounting extension + IAccountingExtension public accountingExtension; + // A mock tree verifier + ITreeVerifier public treeVerifier; + // Mock addresses + IERC20 public _token = IERC20(makeAddr('token')); + + // Mock request data + bytes32[32] internal _treeBranches = [ + bytes32('branch1'), + bytes32('branch2'), + bytes32('branch3'), + bytes32('branch4'), + bytes32('branch5'), + bytes32('branch6'), + bytes32('branch7'), + bytes32('branch8'), + bytes32('branch9'), + bytes32('branch10'), + bytes32('branch11'), + bytes32('branch12'), + bytes32('branch13'), + bytes32('branch14'), + bytes32('branch15'), + bytes32('branch16'), + bytes32('branch17'), + bytes32('branch18'), + bytes32('branch19'), + bytes32('branch20'), + bytes32('branch21'), + bytes32('branch22'), + bytes32('branch23'), + bytes32('branch24'), + bytes32('branch25'), + bytes32('branch26'), + bytes32('branch27'), + bytes32('branch28'), + bytes32('branch29'), + bytes32('branch30'), + bytes32('branch31'), + bytes32('branch32') + ]; + uint256 internal _treeCount = 1; + bytes internal _treeData = abi.encode(_treeBranches, _treeCount); + bytes32[] internal _leavesToInsert = [bytes32('leave1'), bytes32('leave2')]; + + // Events + event ResponseDisputed( + bytes32 indexed _requestId, + bytes32 indexed _responseId, + bytes32 indexed _disputeId, + IOracle.Dispute _dispute, + uint256 _blockNumber + ); + + /** + * @notice Deploy the target and mock oracle+accounting extension + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + accountingExtension = IAccountingExtension(makeAddr('AccountingExtension')); + vm.etch(address(accountingExtension), hex'069420'); + + treeVerifier = ITreeVerifier(makeAddr('TreeVerifier')); + vm.etch(address(treeVerifier), hex'069420'); + + rootVerificationModule = new RootVerificationModule(oracle); + } +} + +contract RootVerificationModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(rootVerificationModule.moduleName(), 'RootVerificationModule'); + } + + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData_returnsCorrectData( + address _accountingExtension, + address _randomToken, + uint256 _bondSize + ) public { + // Mock data + bytes memory _requestData = abi.encode( + IRootVerificationModule.RequestParameters({ + treeData: _treeData, + leavesToInsert: _leavesToInsert, + treeVerifier: treeVerifier, + accountingExtension: IAccountingExtension(_accountingExtension), + bondToken: IERC20(_randomToken), + bondSize: _bondSize + }) + ); + + IRootVerificationModule.RequestParameters memory _params = rootVerificationModule.decodeRequestData(_requestData); + + bytes32[32] memory _treeBranchesStored; + uint256 _treeCountStored; + (_treeBranchesStored, _treeCountStored) = abi.decode(_params.treeData, (bytes32[32], uint256)); + + // Check: is the request data properly stored? + for (uint256 _i = 0; _i < _treeBranches.length; _i++) { + assertEq(_treeBranchesStored[_i], _treeBranches[_i], 'Mismatch: decoded tree branch'); + } + for (uint256 _i = 0; _i < _leavesToInsert.length; _i++) { + assertEq(_params.leavesToInsert[_i], _leavesToInsert[_i], 'Mismatch: decoded leave to insert'); + } + assertEq(_treeCountStored, _treeCount, 'Mismatch: decoded tree count'); + assertEq(address(_params.treeVerifier), address(treeVerifier), 'Mismatch: decoded tree verifier'); + assertEq(address(_params.accountingExtension), _accountingExtension, 'Mismatch: decoded accounting extension'); + assertEq(address(_params.bondToken), _randomToken, 'Mismatch: decoded token'); + assertEq(_params.bondSize, _bondSize, 'Mismatch: decoded bond size'); + } +} + +contract RootVerificationModule_Unit_DisputeResponse is BaseTest { + /** + * @notice Test if dispute incorrect response returns the correct status + */ + function test_disputeIncorrectResponse( + bytes memory _treeData, + bytes32[] calldata _leavesToInsert, + IAccountingExtension _accountingExtension, + uint256 _bondSize + ) public { + mockRequest.disputeModuleData = abi.encode( + IRootVerificationModule.RequestParameters({ + treeData: _treeData, + leavesToInsert: _leavesToInsert, + treeVerifier: treeVerifier, + accountingExtension: _accountingExtension, + bondToken: _token, + bondSize: _bondSize + }) + ); + + // Create new Response memory struct with random values + mockResponse.response = abi.encode(bytes32('randomRoot')); + + // Mock and expect the call to the tree verifier, calculating the root + _mockAndExpect( + address(treeVerifier), + abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), + abi.encode(bytes32('randomRoot2')) + ); + + // Mock and expect the call the oracle, updating the dispute's status + _mockAndExpect( + address(oracle), + abi.encodeWithSelector( + oracle.updateDisputeStatus.selector, mockRequest, mockResponse, mockDispute, IOracle.DisputeStatus.Won + ), + abi.encode(true) + ); + + vm.prank(address(oracle)); + rootVerificationModule.disputeResponse(mockRequest, mockResponse, mockDispute); + } + + function test_emitsEvent( + bytes memory _treeData, + bytes32[] calldata _leavesToInsert, + IAccountingExtension _accountingExtension, + uint256 _bondSize + ) public { + mockRequest.disputeModuleData = abi.encode( + IRootVerificationModule.RequestParameters({ + treeData: _treeData, + leavesToInsert: _leavesToInsert, + treeVerifier: treeVerifier, + accountingExtension: _accountingExtension, + bondToken: _token, + bondSize: _bondSize + }) + ); + + // Create new Response memory struct with random values + mockResponse.response = abi.encode(bytes32('randomRoot')); + mockResponse.requestId = _getId(mockRequest); + mockDispute.requestId = _getId(mockRequest); + mockDispute.responseId = _getId(mockResponse); + + // Mock and expect the call to the tree verifier, calculating the root + _mockAndExpect( + address(treeVerifier), + abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), + abi.encode(bytes32('randomRoot2')) + ); + + // Mock and expect the call the oracle, updating the dispute's status + _mockAndExpect( + address(oracle), + abi.encodeWithSelector( + oracle.updateDisputeStatus.selector, mockRequest, mockResponse, mockDispute, IOracle.DisputeStatus.Won + ), + abi.encode(true) + ); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(rootVerificationModule)); + emit ResponseDisputed({ + _requestId: mockDispute.requestId, + _responseId: mockDispute.responseId, + _disputeId: _getId(mockDispute), + _dispute: mockDispute, + _blockNumber: block.number + }); + + vm.prank(address(oracle)); + rootVerificationModule.disputeResponse(mockRequest, mockResponse, mockDispute); + } + + /** + * @notice Test if dispute correct response returns the correct status + */ + function test_disputeCorrectResponse( + bytes memory _treeData, + bytes32[] calldata _leavesToInsert, + IAccountingExtension _accountingExtension, + uint256 _bondSize + ) public { + mockRequest.disputeModuleData = abi.encode( + IRootVerificationModule.RequestParameters({ + treeData: _treeData, + leavesToInsert: _leavesToInsert, + treeVerifier: treeVerifier, + accountingExtension: _accountingExtension, + bondToken: _token, + bondSize: _bondSize + }) + ); + + bytes memory _encodedCorrectRoot = abi.encode(bytes32('randomRoot')); + + // Create new Response memory struct with random values + mockResponse.response = _encodedCorrectRoot; + mockResponse.requestId = _getId(mockRequest); + + // Mock and expect the call to the tree verifier, calculating the root + _mockAndExpect( + address(treeVerifier), + abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), + _encodedCorrectRoot + ); + + // Mock and expect the call the oracle, updating the dispute's status + _mockAndExpect( + address(oracle), + abi.encodeWithSelector( + oracle.updateDisputeStatus.selector, mockRequest, mockResponse, mockDispute, IOracle.DisputeStatus.Lost + ), + abi.encode(true) + ); + + vm.prank(address(oracle)); + rootVerificationModule.disputeResponse(mockRequest, mockResponse, mockDispute); + } + + /** + * @notice Test if dispute response reverts when called by caller who's not the oracle + */ + function test_revertWrongCaller(address _randomCaller) public { + vm.assume(_randomCaller != address(oracle)); + + // Check: revert if not called by the Oracle? + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + + vm.prank(_randomCaller); + rootVerificationModule.disputeResponse(mockRequest, mockResponse, mockDispute); + } +} diff --git a/yarn.lock b/yarn.lock index 4e936a9c..f3d82dcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1181,14 +1181,10 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -"ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": - version "1.0.0" - resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" - -"ds-test@https://github.com/dapphub/ds-test": +"ds-test@git+https://github.com/dapphub/ds-test.git", "ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" uid e282159d5170298eb2455a6c05280ab5a73a4ef0 - resolved "https://github.com/dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0" + resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" "ds-test@https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" @@ -1570,14 +1566,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +"forge-std@git+https://github.com/foundry-rs/forge-std.git": + version "1.7.1" + resolved "git+https://github.com/foundry-rs/forge-std.git#37a37ab73364d6644bfe11edf88a07880f99bd56" + "forge-std@git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e": version "1.5.6" resolved "git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e" -"forge-std@https://github.com/foundry-rs/forge-std": - version "1.7.1" - resolved "https://github.com/foundry-rs/forge-std#37a37ab73364d6644bfe11edf88a07880f99bd56" - "forge-std@https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421": version "1.7.1" resolved "https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421" @@ -3265,9 +3261,9 @@ solhint@3.5.1: prettier "^2.8.3" solidity-ast@^0.4.38: - version "0.4.52" - resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.52.tgz#9f1a9abc7e5ba28bbf91146ecd07aec7e70f3c85" - integrity sha512-iOya9BSiB9jhM8Vf40n8lGELGzwrUc57rl5BhfNtJ5cvAaMvRcNlHeAMNvqJJyjoUnczqRbHqdivEqK89du3Cw== + version "0.4.53" + resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.53.tgz#098259472fccd234ff00f050afaf7843a7ccd635" + integrity sha512-/7xYF//mAt4iP9S21fCFSLMouYXzXJqrd84jbI1LHL1rq7XhyFLUXxVcRkl9KqEEQmI+DmDbTeS6W5cEwcJ2wQ== dependencies: array.prototype.findlast "^1.2.2"