diff --git a/docs/src/content/modules/dispute/circuit_resolver_module.md b/docs/src/content/modules/dispute/circuit_resolver_module.md index 4806899e..81ac72f4 100644 --- a/docs/src/content/modules/dispute/circuit_resolver_module.md +++ b/docs/src/content/modules/dispute/circuit_resolver_module.md @@ -10,10 +10,9 @@ The Circuit Resolver Module is a pre-dispute module that allows disputers to ver ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: Returns the decoded data for a request. -- `disputeResponse(bytes32 _requestId, bytes32 _responseId, address _disputer, address _proposer)`: Verifies the ZK circuit 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 circuit 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`: Verifies the ZK circuit 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 circuit as a response and finalizing the request. ### Request Parameters diff --git a/solidity/contracts/modules/dispute/CircuitResolverModule.sol b/solidity/contracts/modules/dispute/CircuitResolverModule.sol index 015d8a76..8518a2b4 100644 --- a/solidity/contracts/modules/dispute/CircuitResolverModule.sol +++ b/solidity/contracts/modules/dispute/CircuitResolverModule.sol @@ -1,91 +1,93 @@ -// // 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 {ICircuitResolverModule} from '../../../interfaces/modules/dispute/ICircuitResolverModule.sol'; - -// contract CircuitResolverModule is Module, ICircuitResolverModule { -// constructor(IOracle _oracle) Module(_oracle) {} - -// mapping(bytes32 _requestId => bytes _correctResponse) internal _correctResponses; - -// /// @inheritdoc IModule -// function moduleName() external pure returns (string memory _moduleName) { -// return 'CircuitResolverModule'; -// } - -// /// @inheritdoc ICircuitResolverModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } - -// /// @inheritdoc ICircuitResolverModule -// function disputeEscalated(bytes32 _disputeId) external onlyOracle {} - -// /// @inheritdoc ICircuitResolverModule -// function onDisputeStatusChange(bytes32, /* _disputeId */ IOracle.Dispute memory _dispute) external onlyOracle { -// RequestParameters memory _params = decodeRequestData(_dispute.requestId); - -// IOracle.Response memory _response = ORACLE.getResponse(_dispute.responseId); - -// bytes memory _correctResponse = _correctResponses[_dispute.requestId]; -// bool _won = _response.response.length != _correctResponse.length -// || keccak256(_response.response) != keccak256(_correctResponse); - -// 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(_correctResponses[_dispute.requestId])); -// ORACLE.finalize(_dispute.requestId, _correctResponseId); -// } else { -// ORACLE.finalize(_dispute.requestId, _dispute.responseId); -// } - -// delete _correctResponses[_dispute.requestId]; - -// emit DisputeStatusChanged({ -// _requestId: _dispute.requestId, -// _responseId: _dispute.responseId, -// _disputer: _dispute.disputer, -// _proposer: _dispute.proposer, -// _status: _dispute.status -// }); -// } - -// /// @inheritdoc ICircuitResolverModule -// 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); - -// (, bytes memory _correctResponse) = _params.verifier.call(_params.callData); -// _correctResponses[_requestId] = _correctResponse; - -// bool _won = _response.response.length != _correctResponse.length -// || keccak256(_response.response) != keccak256(_correctResponse); - -// _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 {ICircuitResolverModule} from '../../../interfaces/modules/dispute/ICircuitResolverModule.sol'; + +contract CircuitResolverModule is Module, ICircuitResolverModule { + constructor(IOracle _oracle) Module(_oracle) {} + + mapping(bytes32 _requestId => bytes _correctResponse) internal _correctResponses; + + /// @inheritdoc IModule + function moduleName() external pure returns (string memory _moduleName) { + return 'CircuitResolverModule'; + } + + /// @inheritdoc ICircuitResolverModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } + + /// @inheritdoc ICircuitResolverModule + 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 instead of reading from `_correctResponses` + RequestParameters memory _params = decodeRequestData(_request.disputeModuleData); + + bytes memory _correctResponse = _correctResponses[_dispute.requestId]; + bool _won = _response.response.length != _correctResponse.length + || keccak256(_response.response) != keccak256(_correctResponse); + + 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: _correctResponse}); + + 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 _correctResponses[_dispute.requestId]; + } + + /// @inheritdoc ICircuitResolverModule + function disputeResponse( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.disputeModuleData); + + (bool _success, bytes memory _correctResponse) = _params.verifier.call(_params.callData); + + if (!_success) revert CircuitResolverModule_VerificationFailed(); + + _correctResponses[_response.requestId] = _correctResponse; + + IOracle.DisputeStatus _status = _response.response.length != _correctResponse.length + || keccak256(_response.response) != keccak256(_correctResponse) + ? 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/ICircuitResolverModule.sol b/solidity/interfaces/modules/dispute/ICircuitResolverModule.sol index fc224ce0..ae3272fd 100644 --- a/solidity/interfaces/modules/dispute/ICircuitResolverModule.sol +++ b/solidity/interfaces/modules/dispute/ICircuitResolverModule.sol @@ -1,68 +1,94 @@ -// // 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 {IAccountingExtension} from '../../extensions/IAccountingExtension.sol'; +import {IAccountingExtension} from '../../extensions/IAccountingExtension.sol'; -// /** -// * @title CircuitResolverModule -// * @notice Module allowing users to dispute a proposed response -// * by bonding tokens. -// * The module will invoke the circuit verifier supplied to calculate -// * the proposed response and compare it to the correct response. -// * - If the dispute is valid, the disputer wins and their bond is returned along with a reward. -// * - If the dispute is invalid, the bond is forfeited and returned to the proposer. -// * -// * After the dispute is settled, the correct response is automatically proposed to the oracle -// * and the request is finalized. -// */ -// interface ICircuitResolverModule is IDisputeModule { -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ +/** + * @title CircuitResolverModule + * @notice Module allowing users to dispute a proposed response by bonding tokens. + * The module will invoke the circuit verifier supplied to calculate + * the proposed response and compare it to the correct response. + * - If the dispute is valid, the disputer wins and their bond is returned along with a reward. + * - If the dispute is invalid, the bond is forfeited and returned to the proposer. + * + * After the dispute is settled, the correct response is automatically proposed to the oracle + * and the request is finalized. + */ +interface ICircuitResolverModule is IDisputeModule { + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Parameters of the request as stored in the module -// * @return callData The encoded data forwarded to the verifier -// * @return verifier The address of the verifier contract -// * @return accountingExtension The address of the accounting extension -// * @return bondToken The address of the bond token -// * @return bondSize The size of the bond -// */ -// struct RequestParameters { -// bytes callData; -// address verifier; -// IAccountingExtension accountingExtension; -// IERC20 bondToken; -// uint256 bondSize; -// } + /** + * @notice Parameters of the request as stored in the module + * + * @return callData The encoded data forwarded to the verifier + * @return verifier The address of the verifier contract + * @return accountingExtension The address of the accounting extension + * @return bondToken The address of the bond token + * @return bondSize The size of the bond + */ + struct RequestParameters { + bytes callData; + address verifier; + IAccountingExtension accountingExtension; + IERC20 bondToken; + uint256 bondSize; + } -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Returns the decoded data for a request -// * @param _requestId The ID of the request -// * @return _params The decoded parameters of the request -// */ -// function decodeRequestData(bytes32 _requestId) external view returns (RequestParameters memory _params); + /** + * @notice Thrown when the verification of a response fails + */ + error CircuitResolverModule_VerificationFailed(); -// /// @inheritdoc IDisputeModule -// function disputeResponse( -// bytes32 _requestId, -// bytes32 _responseId, -// address _disputer, -// address _proposer -// ) external returns (IOracle.Dispute memory _dispute); + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ -// /// @inheritdoc IDisputeModule -// function onDisputeStatusChange(bytes32 _disputeId, IOracle.Dispute memory _dispute) external; + /** + * @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); -// /// @inheritdoc IDisputeModule -// function disputeEscalated(bytes32 _disputeId) external; -// } + /** + * @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 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 + * @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/mocks/MockVerifier.sol b/solidity/test/mocks/MockVerifier.sol new file mode 100644 index 00000000..829887c1 --- /dev/null +++ b/solidity/test/mocks/MockVerifier.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ITreeVerifier} from '../../interfaces/ITreeVerifier.sol'; + +contract MockVerifier is ITreeVerifier { + constructor() {} + + function calculateRoot( + bytes memory, /* _treeData */ + bytes32[] memory /* _leavesToInsert */ + ) external view returns (bytes32 _calculatedRoot) { + _calculatedRoot = keccak256(abi.encode(block.timestamp)); + } +} diff --git a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol index 36e325fa..5cc4878a 100644 --- a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol +++ b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol @@ -1,477 +1,298 @@ -// // 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 { -// CircuitResolverModule, -// ICircuitResolverModule -// } from '../../../../contracts/modules/dispute/CircuitResolverModule.sol'; - -// import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; - -// /** -// * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks -// */ -// contract ForTest_CircuitResolverModule is CircuitResolverModule { -// constructor(IOracle _oracle) CircuitResolverModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } - -// function forTest_setCorrectResponse(bytes32 _requestId, bytes memory _data) public { -// _correctResponses[_requestId] = _data; -// } -// } - -// /** -// * @title Bonded Dispute Module Unit tests -// */ -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_CircuitResolverModule public circuitResolverModule; -// // 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 circuit verifier address -// address public circuitVerifier; -// // Mock addresses -// IERC20 public _token = IERC20(makeAddr('token')); -// address public _disputer = makeAddr('disputer'); -// address public _proposer = makeAddr('proposer'); -// bytes internal _callData = abi.encodeWithSignature('test(uint256)', 123); - -// event ResponseDisputed(bytes32 indexed _requestId, bytes32 _responseId, address _disputer, address _proposer); -// event DisputeStatusChanged( -// bytes32 indexed _requestId, bytes32 _responseId, address _disputer, address _proposer, IOracle.DisputeStatus _status -// ); - -// /** -// * @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'); -// circuitVerifier = makeAddr('CircuitVerifier'); -// vm.etch(address(circuitVerifier), hex'069420'); - -// circuitResolverModule = new ForTest_CircuitResolverModule(oracle); - -// mockDispute = IOracle.Dispute({ -// createdAt: block.timestamp, -// disputer: dude, -// responseId: mockId, -// proposer: dude, -// requestId: mockId, -// status: IOracle.DisputeStatus.Active -// }); -// } -// } - -// contract CircuitResolverModule_Unit_ModuleData is BaseTest { -// /** -// * @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( -// ICircuitResolverModule.RequestParameters({ -// callData: _callData, -// verifier: circuitVerifier, -// accountingExtension: IAccountingExtension(_accountingExtension), -// bondToken: IERC20(_randomToken), -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// circuitResolverModule.forTest_setRequestData(_requestId, _requestData); - -// // Test: decode the given request data -// ICircuitResolverModule.RequestParameters memory _params = circuitResolverModule.decodeRequestData(_requestId); - -// // Check: is the request data properly stored? -// assertEq(_params.callData, _callData, 'Mismatch: decoded calldata'); -// assertEq(_params.verifier, circuitVerifier, 'Mismatch: decoded circuit 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'); -// } - -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(circuitResolverModule.moduleName(), 'CircuitResolverModule'); -// } -// } - -// contract CircuitResolverModule_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); -// bool _correctResponse = false; - -// // Mock request data -// bytes memory _requestData = abi.encode( -// ICircuitResolverModule.RequestParameters({ -// callData: _callData, -// verifier: circuitVerifier, -// accountingExtension: accountingExtension, -// bondToken: IERC20(_token), -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// circuitResolverModule.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(true) -// }); - -// // 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 verifier -// _mockAndExpect(circuitVerifier, _callData, abi.encode(_correctResponse)); - -// // Test: call disputeResponse -// vm.prank(address(oracle)); -// IOracle.Dispute memory _dispute = -// circuitResolverModule.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); - -// bool _correctResponse = false; - -// // Mock request data -// bytes memory _requestData = abi.encode( -// ICircuitResolverModule.RequestParameters({ -// callData: _callData, -// verifier: circuitVerifier, -// accountingExtension: accountingExtension, -// bondToken: IERC20(_token), -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// circuitResolverModule.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(true) -// }); - -// // 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 verifier -// _mockAndExpect(circuitVerifier, _callData, abi.encode(_correctResponse)); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(circuitResolverModule)); -// emit ResponseDisputed(_requestId, _responseId, _disputer, _proposer); - -// vm.prank(address(oracle)); -// circuitResolverModule.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 _encodedCorrectResponse = abi.encode(true); - -// // Mock request data -// bytes memory _requestData = abi.encode( -// ICircuitResolverModule.RequestParameters({ -// callData: _callData, -// verifier: circuitVerifier, -// accountingExtension: accountingExtension, -// bondToken: IERC20(_token), -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// circuitResolverModule.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: _encodedCorrectResponse -// }); - -// // 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 verifier -// _mockAndExpect(circuitVerifier, _callData, _encodedCorrectResponse); - -// vm.prank(address(oracle)); -// IOracle.Dispute memory _dispute = -// circuitResolverModule.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: does it revert if not called by the Oracle? -// vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); - -// vm.prank(_randomCaller); -// circuitResolverModule.disputeResponse(mockId, mockId, dude, dude); -// } -// } - -// contract CircuitResolverModule_Unit_DisputeEscalation is BaseTest { -// /** -// * @notice Test if dispute escalated do nothing -// */ -// function test_returnCorrectStatus() public { -// // Record sstore and sload -// vm.prank(address(oracle)); -// vm.record(); -// circuitResolverModule.disputeEscalated(mockId); -// (bytes32[] memory _reads, bytes32[] memory _writes) = vm.accesses(address(circuitResolverModule)); - -// // Check: no storage access? -// assertEq(_reads.length, 0); -// assertEq(_writes.length, 0); -// } - -// /** -// * @notice Test that escalateDispute finalizes the request if the original response is correct -// */ -// function test_correctResponse(bytes32 _requestId, bytes32 _responseId, uint256 _bondSize) public { -// vm.assume(_requestId != _responseId); - -// bytes memory _encodedCorrectResponse = abi.encode(true); - -// // Mock request data -// bytes memory _requestData = abi.encode( -// ICircuitResolverModule.RequestParameters({ -// callData: _callData, -// verifier: circuitVerifier, -// accountingExtension: accountingExtension, -// bondToken: IERC20(_token), -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// circuitResolverModule.forTest_setRequestData(_requestId, _requestData); - -// circuitResolverModule.forTest_setCorrectResponse(_requestId, _encodedCorrectResponse); - -// // Create new Response memory struct with random values -// IOracle.Response memory _mockResponse = IOracle.Response({ -// createdAt: block.timestamp, -// proposer: _proposer, -// requestId: _requestId, -// disputeId: mockId, -// response: _encodedCorrectResponse -// }); - -// // 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 oracle, finalizing the request -// _mockAndExpect( -// address(oracle), abi.encodeWithSignature('finalize(bytes32,bytes32)', _requestId, _responseId), abi.encode() -// ); - -// // Populate the mock dispute with the correct values -// mockDispute.status = IOracle.DisputeStatus.Lost; -// mockDispute.responseId = _responseId; -// mockDispute.requestId = _requestId; - -// vm.prank(address(oracle)); -// circuitResolverModule.onDisputeStatusChange(bytes32(0), mockDispute); -// } - -// /** -// * @notice Test that escalateDispute pays the disputer and proposes the new response -// */ -// function test_incorrectResponse(bytes32 _requestId, bytes32 _responseId, uint256 _bondSize) public { -// vm.assume(_requestId != _responseId); - -// bytes32 _correctResponseId = bytes32(uint256(mockId) + 2); -// bytes memory _encodedCorrectResponse = abi.encode(true); - -// // Mock request data -// bytes memory _requestData = abi.encode( -// ICircuitResolverModule.RequestParameters({ -// callData: _callData, -// verifier: circuitVerifier, -// accountingExtension: accountingExtension, -// bondToken: IERC20(_token), -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request and correct response -// circuitResolverModule.forTest_setRequestData(_requestId, _requestData); -// circuitResolverModule.forTest_setCorrectResponse(_requestId, _encodedCorrectResponse); - -// // 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(false) -// }); - -// // 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 accounting extension, paying the disputer -// _mockAndExpect( -// address(accountingExtension), -// abi.encodeCall(accountingExtension.pay, (_requestId, _proposer, _disputer, _token, _bondSize)), -// abi.encode() -// ); - -// // Mock and expect the call to the oracle, proposing the correct response with the disputer as the new proposer -// _mockAndExpect( -// address(oracle), -// abi.encodeWithSignature( -// 'proposeResponse(address,bytes32,bytes)', _disputer, _requestId, abi.encode(_encodedCorrectResponse) -// ), -// abi.encode(_correctResponseId) -// ); - -// // Mock and expect the call to the oracle, finalizing the request with the correct response -// _mockAndExpect( -// address(oracle), -// abi.encodeWithSignature('finalize(bytes32,bytes32)', _requestId, _correctResponseId), -// abi.encode() -// ); - -// // Populate the mock dispute with the correct values -// mockDispute.status = IOracle.DisputeStatus.Won; -// mockDispute.responseId = _responseId; -// mockDispute.requestId = _requestId; -// mockDispute.disputer = _disputer; -// mockDispute.proposer = _proposer; - -// vm.prank(address(oracle)); -// circuitResolverModule.onDisputeStatusChange(bytes32(0), mockDispute); -// } -// } - -// contract CircuitResolverModule_Unit_OnDisputeStatusChange is BaseTest { -// function test_eventEmitted(bytes32 _requestId, bytes32 _responseId, uint256 _bondSize) public { -// vm.assume(_requestId != _responseId); - -// bytes memory _encodedCorrectResponse = abi.encode(true); - -// // Mock request data -// bytes memory _requestData = abi.encode( -// ICircuitResolverModule.RequestParameters({ -// callData: _callData, -// verifier: circuitVerifier, -// accountingExtension: accountingExtension, -// bondToken: IERC20(_token), -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// circuitResolverModule.forTest_setRequestData(_requestId, _requestData); -// circuitResolverModule.forTest_setCorrectResponse(_requestId, _encodedCorrectResponse); - -// // Create new Response memory struct with random values -// IOracle.Response memory _mockResponse = IOracle.Response({ -// createdAt: block.timestamp, -// proposer: _proposer, -// requestId: _requestId, -// disputeId: mockId, -// response: _encodedCorrectResponse -// }); - -// // 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 oracle, finalizing the request -// _mockAndExpect( -// address(oracle), abi.encodeWithSignature('finalize(bytes32,bytes32)', _requestId, _responseId), abi.encode() -// ); - -// // Populate the mock dispute with the correct values -// mockDispute.status = IOracle.DisputeStatus.Lost; -// mockDispute.responseId = _responseId; -// mockDispute.requestId = _requestId; - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(circuitResolverModule)); -// emit DisputeStatusChanged( -// _requestId, _responseId, mockDispute.disputer, mockDispute.proposer, IOracle.DisputeStatus.Lost -// ); - -// vm.prank(address(oracle)); -// circuitResolverModule.onDisputeStatusChange(bytes32(0), mockDispute); -// } -// } +// 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 { + CircuitResolverModule, + ICircuitResolverModule +} from '../../../../contracts/modules/dispute/CircuitResolverModule.sol'; + +import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; +import {MockVerifier} from '../../../mocks/MockVerifier.sol'; + +/** + * @dev Harness to set an entry in the correctResponses mapping + */ +contract ForTest_CircuitResolverModule is CircuitResolverModule { + constructor(IOracle _oracle) CircuitResolverModule(_oracle) {} + + function forTest_setCorrectResponse(bytes32 _requestId, bytes memory _data) public { + _correctResponses[_requestId] = _data; + } +} + +/** + * @title Bonded Dispute Module Unit tests + */ +contract BaseTest is Test, Helpers { + // The target contract + ForTest_CircuitResolverModule public circuitResolverModule; + // A mock oracle + IOracle public oracle; + // A mock circuit verifier address + MockVerifier public mockVerifier; + + // Events + event DisputeStatusChanged(bytes32 _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); + 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'); + + mockVerifier = new MockVerifier(); + + circuitResolverModule = new ForTest_CircuitResolverModule(oracle); + } +} + +contract CircuitResolverModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData( + IAccountingExtension _accountingExtension, + IERC20 _randomToken, + uint256 _bondSize, + bytes memory _callData + ) public { + // Mock data + bytes memory _requestData = abi.encode( + ICircuitResolverModule.RequestParameters({ + callData: _callData, + verifier: address(mockVerifier), + accountingExtension: IAccountingExtension(_accountingExtension), + bondToken: IERC20(_randomToken), + bondSize: _bondSize + }) + ); + + // Test: decode the given request data + ICircuitResolverModule.RequestParameters memory _params = circuitResolverModule.decodeRequestData(_requestData); + + // Check: is the request data properly stored? + assertEq( + address(_params.accountingExtension), address(_accountingExtension), 'Mismatch: decoded accounting extension' + ); + assertEq(address(_params.bondToken), address(_randomToken), 'Mismatch: decoded token'); + assertEq(_params.verifier, address(mockVerifier), 'Mismatch: decoded circuit verifier'); + assertEq(_params.bondSize, _bondSize, 'Mismatch: decoded bond size'); + assertEq(_params.callData, _callData, 'Mismatch: decoded calldata'); + } + + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleName() public { + assertEq(circuitResolverModule.moduleName(), 'CircuitResolverModule'); + } +} + +contract CircuitResolverModule_Unit_DisputeResponse is BaseTest { + /** + * @notice Test if dispute incorrect response returns the correct status + */ + function test_disputeIncorrectResponse( + IAccountingExtension _accountingExtension, + IERC20 _randomToken, + uint256 _bondSize, + bytes memory _callData + ) public { + _callData = abi.encodeWithSelector(mockVerifier.calculateRoot.selector, _callData); + + mockRequest.disputeModuleData = abi.encode( + ICircuitResolverModule.RequestParameters({ + callData: _callData, + verifier: address(mockVerifier), + accountingExtension: _accountingExtension, + bondToken: _randomToken, + bondSize: _bondSize + }) + ); + + bool _correctResponse = false; + + mockResponse.requestId = _getId(mockRequest); + mockDispute.requestId = mockResponse.requestId; + mockDispute.responseId = _getId(mockResponse); + + // Mock and expect the call to the verifier + _mockAndExpect(address(mockVerifier), _callData, abi.encode(_correctResponse)); + + // 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) + ); + + // Test: call disputeResponse + vm.prank(address(oracle)); + circuitResolverModule.disputeResponse(mockRequest, mockResponse, mockDispute); + } + + function test_emitsEvent( + IAccountingExtension _accountingExtension, + IERC20 _randomToken, + uint256 _bondSize, + bytes memory _callData + ) public { + mockRequest.disputeModuleData = abi.encode( + ICircuitResolverModule.RequestParameters({ + callData: _callData, + verifier: address(mockVerifier), + accountingExtension: _accountingExtension, + bondToken: _randomToken, + bondSize: _bondSize + }) + ); + + bytes32 _requestId = _getId(mockRequest); + bool _correctResponse = false; + + mockResponse.requestId = _requestId; + mockResponse.response = abi.encode(true); + + // Mock and expect the call to the verifier + _mockAndExpect(address(mockVerifier), _callData, abi.encode(_correctResponse)); + + // 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(circuitResolverModule)); + emit ResponseDisputed({ + _requestId: mockResponse.requestId, + _responseId: mockDispute.responseId, + _disputeId: _getId(mockDispute), + _dispute: mockDispute, + _blockNumber: block.number + }); + + vm.prank(address(oracle)); + circuitResolverModule.disputeResponse(mockRequest, mockResponse, mockDispute); + } + + /** + * @notice Test if dispute correct response returns the correct status + */ + function test_disputeCorrectResponse( + IAccountingExtension _accountingExtension, + IERC20 _randomToken, + uint256 _bondSize, + bytes memory _callData + ) public { + _callData = abi.encodeWithSelector(mockVerifier.calculateRoot.selector, _callData); + + mockRequest.disputeModuleData = abi.encode( + ICircuitResolverModule.RequestParameters({ + callData: _callData, + verifier: address(mockVerifier), + accountingExtension: _accountingExtension, + bondToken: _randomToken, + bondSize: _bondSize + }) + ); + + bytes memory _encodedCorrectResponse = abi.encode(true); + + mockResponse.requestId = _getId(mockRequest); + mockResponse.response = _encodedCorrectResponse; + + // Mock and expect the call to the verifier + _mockAndExpect(address(mockVerifier), _callData, _encodedCorrectResponse); + + // 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)); + circuitResolverModule.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: does it revert if not called by the Oracle? + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + + vm.prank(_randomCaller); + circuitResolverModule.disputeResponse(mockRequest, mockResponse, mockDispute); + } +} + +contract CircuitResolverModule_Unit_OnDisputeStatusChange is BaseTest { + function test_emitsEvent( + IAccountingExtension _accountingExtension, + IERC20 _randomToken, + uint256 _bondSize, + bytes memory _callData + ) public { + mockRequest.disputeModuleData = abi.encode( + ICircuitResolverModule.RequestParameters({ + callData: _callData, + verifier: address(mockVerifier), + accountingExtension: _accountingExtension, + bondToken: _randomToken, + bondSize: _bondSize + }) + ); + + bytes32 _requestId = _getId(mockRequest); + bytes memory _encodedCorrectResponse = abi.encode(true); + + circuitResolverModule.forTest_setCorrectResponse(_requestId, _encodedCorrectResponse); + + mockResponse.requestId = _requestId; + mockResponse.response = _encodedCorrectResponse; + mockResponse.proposer = mockDispute.disputer; + + // Mock and expect the call to the oracle, finalizing the request + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.finalize, (mockRequest, mockResponse)), abi.encode(true)); + + // Populate the mock dispute with the correct values + mockDispute.responseId = _getId(mockResponse); + mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); + IOracle.DisputeStatus _status = IOracle.DisputeStatus.Lost; + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(circuitResolverModule)); + emit DisputeStatusChanged(_disputeId, mockDispute, _status); + + vm.prank(address(oracle)); + circuitResolverModule.onDisputeStatusChange(_disputeId, mockRequest, mockResponse, mockDispute); + } +}