diff --git a/solidity/contracts/modules/CircuitResolverModule.sol b/solidity/contracts/modules/CircuitResolverModule.sol new file mode 100644 index 00000000..121f1157 --- /dev/null +++ b/solidity/contracts/modules/CircuitResolverModule.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {ICircuitResolverModule} from '../../interfaces/modules/ICircuitResolverModule.sol'; +import {IOracle} from '../../interfaces/IOracle.sol'; +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; + +import {Module} from '../Module.sol'; + +contract CircuitResolverModule is Module, ICircuitResolverModule { + constructor(IOracle _oracle) Module(_oracle) {} + + function moduleName() external pure returns (string memory _moduleName) { + return 'CircuitResolverModule'; + } + + function decodeRequestData(bytes32 _requestId) + public + view + returns ( + bytes memory _callData, + address _verifier, + IAccountingExtension _accountingExtension, + IERC20 _bondToken, + uint256 _bondSize + ) + { + (_callData, _verifier, _accountingExtension, _bondToken, _bondSize) = + abi.decode(requestData[_requestId], (bytes, address, IAccountingExtension, IERC20, uint256)); + } + + function disputeEscalated(bytes32 _disputeId) external onlyOracle {} + function updateDisputeStatus(bytes32, /* _disputeId */ IOracle.Dispute memory _dispute) external onlyOracle {} + + function disputeResponse( + bytes32 _requestId, + bytes32 _responseId, + address _disputer, + address _proposer + ) external onlyOracle returns (IOracle.Dispute memory _dispute) { + IOracle.Response memory _response = ORACLE.getResponse(_responseId); + + ( + bytes memory _proposedResponse, + bytes memory _correctResponse, + IAccountingExtension _accountingExtension, + IERC20 _bondToken, + uint256 _bondSize + ) = _getDisputeData(_requestId, _response); + + // We test length first to short circuit and don't do keccack256 + // if lengths are different, just to optimize gas usage + bool _won = + _proposedResponse.length != _correctResponse.length || keccak256(_proposedResponse) != keccak256(_correctResponse); + + _dispute = IOracle.Dispute({ + disputer: _disputer, + responseId: _responseId, + proposer: _proposer, + requestId: _requestId, + status: _won ? IOracle.DisputeStatus.Won : IOracle.DisputeStatus.Lost, + createdAt: block.timestamp + }); + + if (_won) { + _accountingExtension.pay(_dispute.requestId, _dispute.proposer, _dispute.disputer, _bondToken, _bondSize); + _accountingExtension.release(_dispute.disputer, _dispute.requestId, _bondToken, _bondSize); + bytes32 _correctResponseId = ORACLE.proposeResponse(_disputer, _requestId, abi.encode(_correctResponse)); + ORACLE.finalize(_requestId, _correctResponseId); + } else { + _accountingExtension.release(_dispute.proposer, _dispute.requestId, _bondToken, _bondSize); + ORACLE.finalize(_requestId, _responseId); + } + } + + function _getDisputeData( + bytes32 _requestId, + IOracle.Response memory _response + ) + internal + returns ( + bytes memory _proposedResponse, + bytes memory _correctResponse, + IAccountingExtension _accountingExtension, + IERC20 _bondToken, + uint256 _bondSize + ) + { + ( + bytes memory _callData, + address _verifier, + IAccountingExtension __accountingExtension, + IERC20 __bondToken, + uint256 __bondSize + ) = decodeRequestData(_requestId); + + _proposedResponse = _response.response; + (, _correctResponse) = _verifier.call(_callData); + + _accountingExtension = __accountingExtension; + _bondToken = __bondToken; + _bondSize = __bondSize; + } +} diff --git a/solidity/contracts/modules/CircuitResolverRequestModule.sol b/solidity/contracts/modules/CircuitResolverRequestModule.sol new file mode 100644 index 00000000..5ff96c9d --- /dev/null +++ b/solidity/contracts/modules/CircuitResolverRequestModule.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {ICircuitResolverRequestModule} from '../../interfaces/modules/ICircuitResolverRequestModule.sol'; +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; +import {IOracle} from '../../interfaces/IOracle.sol'; +import {IModule, Module} from '../Module.sol'; + +contract CircuitResolverRequestModule is Module, ICircuitResolverRequestModule { + constructor(IOracle _oracle) Module(_oracle) {} + + function decodeRequestData(bytes32 _requestId) + public + view + returns ( + bytes memory _callData, + address _verifier, + IAccountingExtension _accountingExtension, + IERC20 _paymentToken, + uint256 _paymentAmount + ) + { + (_callData, _verifier, _accountingExtension, _paymentToken, _paymentAmount) = + abi.decode(requestData[_requestId], (bytes, address, IAccountingExtension, IERC20, uint256)); + } + + function _afterSetupRequest(bytes32 _requestId, bytes calldata _data) internal override { + (,, IAccountingExtension _accountingExtension, IERC20 _paymentToken, uint256 _paymentAmount) = + decodeRequestData(_requestId); + IOracle.Request memory _request = ORACLE.getRequest(_requestId); + _accountingExtension.bond(_request.requester, _requestId, _paymentToken, _paymentAmount); + } + + function finalizeRequest(bytes32 _requestId, address) external override(IModule, Module) onlyOracle { + IOracle.Request memory _request = ORACLE.getRequest(_requestId); + IOracle.Response memory _response = ORACLE.getFinalizedResponse(_requestId); + (,, IAccountingExtension _accountingExtension, IERC20 _paymentToken, uint256 _paymentAmount) = + decodeRequestData(_requestId); + if (_response.createdAt != 0) { + _accountingExtension.pay(_requestId, _request.requester, _response.proposer, _paymentToken, _paymentAmount); + } else { + _accountingExtension.release(_request.requester, _requestId, _paymentToken, _paymentAmount); + } + } + + function moduleName() public pure returns (string memory _moduleName) { + _moduleName = 'CircuitResolverRequestModule'; + } +} diff --git a/solidity/interfaces/modules/ICircuitResolverModule.sol b/solidity/interfaces/modules/ICircuitResolverModule.sol new file mode 100644 index 00000000..b816af86 --- /dev/null +++ b/solidity/interfaces/modules/ICircuitResolverModule.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {IDisputeModule} from './IDisputeModule.sol'; +import {IAccountingExtension} from '../extensions/IAccountingExtension.sol'; + +interface ICircuitResolverModule is IDisputeModule { + error CircuitResolverModule_DisputingCorrectHash(bytes32 _proposedHash); + + function decodeRequestData(bytes32 _requestId) + external + view + returns ( + bytes memory _callData, + address _verifier, + IAccountingExtension _accountingExtension, + IERC20 _bondToken, + uint256 _bondSize + ); +} diff --git a/solidity/interfaces/modules/ICircuitResolverRequestModule.sol b/solidity/interfaces/modules/ICircuitResolverRequestModule.sol new file mode 100644 index 00000000..45940b7d --- /dev/null +++ b/solidity/interfaces/modules/ICircuitResolverRequestModule.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {IRequestModule} from '../../interfaces/modules/IRequestModule.sol'; +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; + +interface ICircuitResolverRequestModule is IRequestModule { + function decodeRequestData(bytes32 _requestId) + external + view + returns ( + bytes memory _callData, + address _verifier, + IAccountingExtension _accountingExtension, + IERC20 _paymentToken, + uint256 _paymentAmount + ); +} diff --git a/solidity/test/unit/CircuitResolverModule.t.sol b/solidity/test/unit/CircuitResolverModule.t.sol new file mode 100644 index 00000000..1c0297e1 --- /dev/null +++ b/solidity/test/unit/CircuitResolverModule.t.sol @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +// solhint-disable-next-line +import 'forge-std/Test.sol'; + +import { + CircuitResolverModule, + IOracle, + IAccountingExtension, + IERC20 +} from '../../contracts/modules/CircuitResolverModule.sol'; + +import {IModule} from '../../interfaces/IModule.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; + } +} + +/** + * @title Bonded Dispute Module Unit tests + */ +contract CircuitResolverModule_UnitTest is Test { + using stdStorage for StdStorage; + + // 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 internal _token = IERC20(makeAddr('token')); + address internal _disputer = makeAddr('disputer'); + address internal _proposer = makeAddr('proposer'); + + bytes internal _callData = abi.encodeWithSignature('test(uint256)', 123); + + /** + * @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 + }); + } + + /** + * @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(_callData, circuitVerifier, _accountingExtension, _randomtoken, _bondSize); + + // Store the mock request + circuitResolverModule.forTest_setRequestData(_requestId, _requestData); + + // Test: decode the given request data + ( + bytes memory _callDataStored, + address _verifierStored, + IAccountingExtension _accountingExtensionStored, + IERC20 _tokenStored, + uint256 _bondSizeStored + ) = circuitResolverModule.decodeRequestData(_requestId); + + assertEq(_callDataStored, _callData, 'Mismatch: decoded calldata'); + assertEq(_verifierStored, circuitVerifier, 'Mismatch: decoded circuit verifier'); + assertEq(address(_accountingExtensionStored), _accountingExtension, 'Mismatch: decoded accounting extension'); + assertEq(address(_tokenStored), _randomtoken, 'Mismatch: decoded token'); + assertEq(_bondSizeStored, _bondSize, 'Mismatch: decoded bond size'); + } + + /** + * @notice Test if dispute escalated do nothing + */ + function test_disputeEscalated_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 if dispute incorrect response returns the correct status + */ + function test_disputeResponse_disputeIncorrectResponse(uint256 _bondSize) public { + // Mock id's (insure they are different) + bytes32 _requestId = mockId; + bytes32 _responseId = bytes32(uint256(mockId) + 1); + bytes32 _newResponseId = bytes32(uint256(mockId) + 2); + bool _correctResponse = false; + + // Mock request data + bytes memory _requestData = abi.encode(_callData, circuitVerifier, accountingExtension, _token, _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 + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getResponse, (_responseId)), abi.encode(_mockResponse)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getResponse, (_responseId))); + + // Mock and expect the call to the verifier + vm.mockCall(circuitVerifier, _callData, abi.encode(_correctResponse)); + vm.expectCall(circuitVerifier, _callData); + + // Mock and expect the call to pay, from¨*proposer to disputer* + vm.mockCall( + address(accountingExtension), + abi.encodeCall(accountingExtension.pay, (_requestId, _proposer, _disputer, _token, _bondSize)), + abi.encode() + ); + vm.expectCall( + address(accountingExtension), + abi.encodeCall(accountingExtension.pay, (_requestId, _proposer, _disputer, _token, _bondSize)) + ); + + // Mock and expect the call to release, to the disputer + vm.mockCall( + address(accountingExtension), + abi.encodeCall(accountingExtension.release, (_disputer, _requestId, _token, _bondSize)), + abi.encode() + ); + vm.expectCall( + address(accountingExtension), + abi.encodeCall(accountingExtension.release, (_disputer, _requestId, _token, _bondSize)) + ); + + // Mock and expect the call to the oracle, proposing the response + vm.mockCall( + address(oracle), + abi.encodeWithSignature( + 'proposeResponse(address,bytes32,bytes)', _disputer, _requestId, abi.encode(abi.encode(_correctResponse)) + ), + abi.encode(_newResponseId) + ); + vm.expectCall( + address(oracle), + abi.encodeWithSignature( + 'proposeResponse(address,bytes32,bytes)', _disputer, _requestId, abi.encode(abi.encode(_correctResponse)) + ) + ); + + vm.mockCall(address(oracle), abi.encodeCall(oracle.finalize, (_requestId, _newResponseId)), abi.encode()); + + // // Test: call disputeResponse + vm.prank(address(oracle)); + IOracle.Dispute memory _dispute = + circuitResolverModule.disputeResponse(_requestId, _responseId, _disputer, _proposer); + + // Check: dispute is correct? + 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'); + } + + /** + * @notice Test if dispute correct response returns the correct status + */ + function test_disputeResponse_disputeCorrectResponse(uint256 _bondSize) public { + // Mock id's (insure they are different) + bytes32 _requestId = mockId; + bytes32 _responseId = bytes32(uint256(mockId) + 1); + bytes memory _encodedCorrectResponse = abi.encode(true); + + // Mock request data + bytes memory _requestData = abi.encode(_callData, circuitVerifier, accountingExtension, _token, _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 + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getResponse, (_responseId)), abi.encode(_mockResponse)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getResponse, (_responseId))); + + // Mock and expect the call to the verifier + vm.mockCall(circuitVerifier, _callData, _encodedCorrectResponse); + vm.expectCall(circuitVerifier, _callData); + + // Mock and expect the call to release, to the proposer + vm.mockCall( + address(accountingExtension), + abi.encodeCall(accountingExtension.release, (_proposer, _requestId, _token, _bondSize)), + abi.encode() + ); + vm.expectCall( + address(accountingExtension), + abi.encodeCall(accountingExtension.release, (_proposer, _requestId, _token, _bondSize)) + ); + + vm.mockCall(address(oracle), abi.encodeCall(oracle.finalize, (_requestId, _responseId)), abi.encode()); + + // Test: call disputeResponse + vm.prank(address(oracle)); + IOracle.Dispute memory _dispute = + circuitResolverModule.disputeResponse(_requestId, _responseId, _disputer, _proposer); + + // Check: dispute is correct? + 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_disputeResponse_revertWrongCaller(address _randomCaller) public { + vm.assume(_randomCaller != address(oracle)); + + // Check: revert if wrong caller + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + + // Test: call disputeResponse from non-oracle address + vm.prank(_randomCaller); + circuitResolverModule.disputeResponse(mockId, mockId, dude, dude); + } + + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(circuitResolverModule.moduleName(), 'CircuitResolverModule'); + } +} diff --git a/solidity/test/unit/CircuitResolverRequestModule.t.sol b/solidity/test/unit/CircuitResolverRequestModule.t.sol new file mode 100644 index 00000000..ef4b0449 --- /dev/null +++ b/solidity/test/unit/CircuitResolverRequestModule.t.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +// solhint-disable-next-line +import 'forge-std/Test.sol'; + +import { + CircuitResolverRequestModule, + IModule, + IOracle, + IAccountingExtension, + IERC20 +} from '../../contracts/modules/CircuitResolverRequestModule.sol'; + +/** + * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks + */ +contract ForTest_CircuitResolverRequestModule is CircuitResolverRequestModule { + constructor(IOracle _oracle) CircuitResolverRequestModule(_oracle) {} + + function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { + requestData[_requestId] = _data; + } +} + +/** + * @title HTTP Request Module Unit tests + */ +contract CircuitResolverRequestModule_UnitTest is Test { + // The target contract + ForTest_CircuitResolverRequestModule public circuitResolverRequestModule; + + // A mock oracle + IOracle public oracle; + + // A mock accounting extension + IAccountingExtension public accounting; + + // A mock user for testing + address _user = makeAddr('user'); + + // A second mock user for testing + address _user2 = makeAddr('user2'); + + // A mock ERC20 token + IERC20 _token = IERC20(makeAddr('ERC20')); + + // Mock data + address internal _verifier = address(_token); + bytes internal _callData = abi.encodeWithSignature('test(uint256)', 123); + + /** + * @notice Deploy the target and mock oracle+accounting extension + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + accounting = IAccountingExtension(makeAddr('AccountingExtension')); + vm.etch(address(accounting), hex'069420'); + + circuitResolverRequestModule = new ForTest_CircuitResolverRequestModule(oracle); + } + + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData(bytes32 _requestId, IERC20 _paymentToken, uint256 _paymentAmount) public { + vm.assume(_requestId != bytes32(0)); + vm.assume(address(_paymentToken) != address(0)); + vm.assume(_paymentAmount > 0); + + bytes memory _requestData = abi.encode(_callData, _verifier, accounting, _paymentToken, _paymentAmount); + + // Set the request data + circuitResolverRequestModule.forTest_setRequestData(_requestId, _requestData); + + // Decode the given request data + ( + bytes memory _decodedCallData, + address _decodedVerifier, + IAccountingExtension _decodedAccountingExtension, + IERC20 _decodedPaymentToken, + uint256 _decodedPaymentAmount + ) = circuitResolverRequestModule.decodeRequestData(_requestId); + + // Check: decoded values match original values? + assertEq(_decodedCallData, _callData, 'Mismatch: decoded data'); + assertEq(_decodedVerifier, _verifier, 'Mismatch: decoded target'); + assertEq(address(_decodedAccountingExtension), address(accounting), 'Mismatch: decoded accounting extension'); + assertEq(address(_decodedPaymentToken), address(_paymentToken), 'Mismatch: decoded payment token'); + assertEq(_decodedPaymentAmount, _paymentAmount, 'Mismatch: decoded payment amount'); + } + + /** + * @notice Test that the afterSetupRequest hook: + * - decodes the request data + * - gets the request from the oracle + * - calls the bond function on the accounting extension + */ + function test_afterSetupRequestTriggered( + bytes32 _requestId, + address _requester, + IERC20 _paymentToken, + uint256 _paymentAmount + ) public { + vm.assume(_requestId != bytes32(0)); + vm.assume(_requester != address(0)); + vm.assume(address(_paymentToken) != address(0)); + vm.assume(_paymentAmount > 0); + + bytes memory _requestData = abi.encode(_callData, _verifier, accounting, _paymentToken, _paymentAmount); + + IOracle.Request memory _fullRequest; + _fullRequest.requester = _requester; + + // Mock and assert ext calls + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId)), abi.encode(_fullRequest)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId))); + + vm.mockCall( + address(accounting), + abi.encodeCall(IAccountingExtension.bond, (_requester, _requestId, _paymentToken, _paymentAmount)), + abi.encode(true) + ); + vm.expectCall( + address(accounting), + abi.encodeCall(IAccountingExtension.bond, (_requester, _requestId, _paymentToken, _paymentAmount)) + ); + + vm.prank(address(oracle)); + circuitResolverRequestModule.setupRequest(_requestId, _requestData); + + // Check: request data was set? + assertEq(circuitResolverRequestModule.requestData(_requestId), _requestData, 'Mismatch: Request data'); + } + + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(circuitResolverRequestModule.moduleName(), 'CircuitResolverRequestModule', 'Wrong module name'); + } + + /** + * @notice Test that finalizeRequest calls: + * - oracle get request + * - oracle get response + * - accounting extension pay + * - accounting extension release + */ + function test_finalizeRequestMakesCalls( + bytes32 _requestId, + address _requester, + address _proposer, + IERC20 _paymentToken, + uint256 _paymentAmount + ) public { + vm.assume(_requestId != bytes32(0)); + vm.assume(_requester != address(0)); + vm.assume(_proposer != address(0)); + vm.assume(address(_paymentToken) != address(0)); + vm.assume(_paymentAmount > 0); + + // Use the correct accounting parameters + bytes memory _requestData = abi.encode(_callData, _verifier, accounting, _paymentToken, _paymentAmount); + + IOracle.Request memory _fullRequest; + _fullRequest.requester = _requester; + + IOracle.Response memory _fullResponse; + _fullResponse.proposer = _proposer; + _fullResponse.createdAt = block.timestamp; + + // Set the request data + circuitResolverRequestModule.forTest_setRequestData(_requestId, _requestData); + + // Mock and assert the calls + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId)), abi.encode(_fullRequest)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId))); + + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId))); + + vm.mockCall( + address(accounting), + abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _paymentToken, _paymentAmount)), + abi.encode() + ); + vm.expectCall( + address(accounting), + abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _paymentToken, _paymentAmount)) + ); + + vm.startPrank(address(oracle)); + circuitResolverRequestModule.finalizeRequest(_requestId, address(oracle)); + + // Test the release flow + _fullResponse.createdAt = 0; + + // Update mock call to return the response with createdAt = 0 + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId))); + + vm.mockCall( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _paymentToken, _paymentAmount)), + abi.encode(true) + ); + + vm.expectCall( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _paymentToken, _paymentAmount)) + ); + + circuitResolverRequestModule.finalizeRequest(_requestId, address(this)); + } + + /** + * @notice Test that the finalizeRequest reverts if caller is not the oracle + */ + function test_finalizeOnlyCalledByOracle(bytes32 _requestId, address _caller) public { + vm.assume(_caller != address(oracle)); + + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + vm.prank(_caller); + circuitResolverRequestModule.finalizeRequest(_requestId, address(_caller)); + } +} diff --git a/solidity/test/unit/RootVerificationModule.t.sol b/solidity/test/unit/RootVerificationModule.t.sol index 2742bfea..0dd6a05a 100644 --- a/solidity/test/unit/RootVerificationModule.t.sol +++ b/solidity/test/unit/RootVerificationModule.t.sol @@ -107,7 +107,7 @@ contract RootVerificationModule_UnitTest is Test { accountingExtension = IAccountingExtension(makeAddr('AccountingExtension')); vm.etch(address(accountingExtension), hex'069420'); treeVerifier = ITreeVerifier(makeAddr('TreeVerifier')); - vm.etch(address(accountingExtension), hex'069420'); + vm.etch(address(treeVerifier), hex'069420'); rootVerificationModule = new ForTest_RootVerificationModule(oracle); @@ -267,7 +267,7 @@ contract RootVerificationModule_UnitTest is Test { 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); + assertEq(_dispute.createdAt, block.timestamp, 'Mismatch: createdAt'); } /** @@ -331,7 +331,7 @@ contract RootVerificationModule_UnitTest is Test { 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); + assertEq(_dispute.createdAt, block.timestamp, 'Mismatch: createdAt'); } /**