diff --git a/docs/src/content/modules/request/contract_call_request_module.md b/docs/src/content/modules/request/contract_call_request_module.md index afbf631a..5ae50f9a 100644 --- a/docs/src/content/modules/request/contract_call_request_module.md +++ b/docs/src/content/modules/request/contract_call_request_module.md @@ -10,8 +10,8 @@ The `ContractCallRequestModule` is a module for requesting on-chain information. ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: This method decodes the request data for a given request ID. It returns the target contract address, the function selector, the encoded arguments of the function to call, the accounting extension to bond and release funds, the payment token, and the payment amount. -- `finalizeRequest(bytes32 _requestId, address)`: This method finalizes a request by paying the response proposer. It is only callable by the oracle. +- `decodeRequestData`: This method decodes the request data for a given request ID. It returns the target contract address, the function selector, the encoded arguments of the function to call, the accounting extension to bond and release funds, the payment token, and the payment amount. +- `finalizeRequest`: This method finalizes a request by paying the response proposer. It is only callable by the oracle. ### Request Parameters diff --git a/solidity/contracts/modules/request/ContractCallRequestModule.sol b/solidity/contracts/modules/request/ContractCallRequestModule.sol index afd6b55b..311487ec 100644 --- a/solidity/contracts/modules/request/ContractCallRequestModule.sol +++ b/solidity/contracts/modules/request/ContractCallRequestModule.sol @@ -1,64 +1,62 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; +// 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'; +// 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 {IContractCallRequestModule} from '../../../interfaces/modules/request/IContractCallRequestModule.sol'; +import {IContractCallRequestModule} from '../../../interfaces/modules/request/IContractCallRequestModule.sol'; -// contract ContractCallRequestModule is Module, IContractCallRequestModule { -// constructor(IOracle _oracle) Module(_oracle) {} +contract ContractCallRequestModule is Module, IContractCallRequestModule { + constructor(IOracle _oracle) Module(_oracle) {} -// /// @inheritdoc IModule -// function moduleName() public pure returns (string memory _moduleName) { -// _moduleName = 'ContractCallRequestModule'; -// } + /// @inheritdoc IModule + function moduleName() public pure returns (string memory _moduleName) { + _moduleName = 'ContractCallRequestModule'; + } -// /// @inheritdoc IContractCallRequestModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } + /// @inheritdoc IContractCallRequestModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } -// /** -// * @notice Bonds the requester's funds through the accounting extension -// * @param _requestId The id of the request being set up -// */ -// function _afterSetupRequest(bytes32 _requestId, bytes calldata) internal override { -// RequestParameters memory _params = decodeRequestData(_requestId); -// IOracle.Request memory _request = ORACLE.getRequest(_requestId); -// _params.accountingExtension.bond({ -// _bonder: _request.requester, -// _requestId: _requestId, -// _token: _params.paymentToken, -// _amount: _params.paymentAmount -// }); -// } + /// @inheritdoc IContractCallRequestModule + function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external onlyOracle { + RequestParameters memory _params = decodeRequestData(_data); -// /// @inheritdoc IContractCallRequestModule -// function finalizeRequest( -// bytes32 _requestId, -// address _finalizer -// ) external override(IContractCallRequestModule, Module) onlyOracle { -// IOracle.Request memory _request = ORACLE.getRequest(_requestId); -// IOracle.Response memory _response = ORACLE.getFinalizedResponse(_requestId); -// RequestParameters memory _params = decodeRequestData(_requestId); -// if (_response.createdAt != 0) { -// _params.accountingExtension.pay({ -// _requestId: _requestId, -// _payer: _request.requester, -// _receiver: _response.proposer, -// _token: _params.paymentToken, -// _amount: _params.paymentAmount -// }); -// } else { -// _params.accountingExtension.release({ -// _bonder: _request.requester, -// _requestId: _requestId, -// _token: _params.paymentToken, -// _amount: _params.paymentAmount -// }); -// } -// emit RequestFinalized(_requestId, _finalizer); -// } -// } + _params.accountingExtension.bond({ + _bonder: _requester, + _requestId: _requestId, + _token: _params.paymentToken, + _amount: _params.paymentAmount + }); + } + + /// @inheritdoc IContractCallRequestModule + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external override(IContractCallRequestModule, Module) onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.requestModuleData); + + if (ORACLE.createdAt(_getId(_response)) != 0) { + _params.accountingExtension.pay({ + _requestId: _response.requestId, + _payer: _request.requester, + _receiver: _response.proposer, + _token: _params.paymentToken, + _amount: _params.paymentAmount + }); + } else { + _params.accountingExtension.release({ + _bonder: _request.requester, + _requestId: _response.requestId, + _token: _params.paymentToken, + _amount: _params.paymentAmount + }); + } + + emit RequestFinalized(_response.requestId, _response, _finalizer); + } +} diff --git a/solidity/interfaces/modules/request/IContractCallRequestModule.sol b/solidity/interfaces/modules/request/IContractCallRequestModule.sol index 1d533d6b..e680fbeb 100644 --- a/solidity/interfaces/modules/request/IContractCallRequestModule.sol +++ b/solidity/interfaces/modules/request/IContractCallRequestModule.sol @@ -1,45 +1,63 @@ -// // 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 {IRequestModule} from -// '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/request/IRequestModule.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IRequestModule} from + '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/request/IRequestModule.sol'; -// import {IAccountingExtension} from '../../../interfaces/extensions/IAccountingExtension.sol'; +import {IAccountingExtension} from '../../../interfaces/extensions/IAccountingExtension.sol'; -// /** -// * @title ContractCallRequestModule -// * @notice Request module for making contract calls -// */ -// interface IContractCallRequestModule is IRequestModule { -// /** -// * @notice Parameters of the request as stored in the module -// * @param target The address of the contract to do the call on -// * @param functionSelector The selector of the function to call -// * @param data The encoded arguments of the function to call (optional) -// * @param accountingExtension The accounting extension to bond and release funds -// * @param paymentToken The token in which the response proposer will be paid -// * @param paymentAmount The amount of `paymentToken` to pay to the response proposer -// */ -// struct RequestParameters { -// address target; -// bytes4 functionSelector; -// bytes data; -// IAccountingExtension accountingExtension; -// IERC20 paymentToken; -// uint256 paymentAmount; -// } +/** + * @title ContractCallRequestModule + * @notice Request module for making contract calls + */ +interface IContractCallRequestModule is IRequestModule { + /** + * @notice Parameters of the request as stored in the module + * @param target The address of the contract to do the call on + * @param functionSelector The selector of the function to call + * @param data The encoded arguments of the function to call (optional) + * @param accountingExtension The accounting extension to bond and release funds + * @param paymentToken The token in which the response proposer will be paid + * @param paymentAmount The amount of `paymentToken` to pay to the response proposer + */ + struct RequestParameters { + address target; + bytes4 functionSelector; + bytes data; + IAccountingExtension accountingExtension; + IERC20 paymentToken; + uint256 paymentAmount; + } -// /** -// * @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 struct containing the parameters for the request + */ + function decodeRequestData(bytes calldata _data) external view returns (RequestParameters memory _params); -// /** -// * @notice Finalizes a request by paying the response proposer -// * @param _requestId The id of the request -// */ -// function finalizeRequest(bytes32 _requestId, address) external; -// } + /** + * @notice Executes pre-request logic, bonding the requester's funds + * + * @param _requestId The id of the request + * @param _data The encoded request parameters + * @param _requester The user who triggered the request + */ + function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external; + + /** + * @notice Finalizes the request by paying the proposer for the response or releasing the requester's bond if no response was submitted + * + * @param _request The request that is being finalized + * @param _response The final response + * @param _finalizer The user who triggered the finalization + */ + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external; +} diff --git a/solidity/test/unit/modules/request/ContractCallRequestModule.t.sol b/solidity/test/unit/modules/request/ContractCallRequestModule.t.sol index 0fcf779a..fa3e3ebe 100644 --- a/solidity/test/unit/modules/request/ContractCallRequestModule.t.sol +++ b/solidity/test/unit/modules/request/ContractCallRequestModule.t.sol @@ -1,310 +1,208 @@ -// // 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 { -// ContractCallRequestModule, -// IContractCallRequestModule -// } from '../../../../contracts/modules/request/ContractCallRequestModule.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_ContractCallRequestModule is ContractCallRequestModule { -// constructor(IOracle _oracle) ContractCallRequestModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } -// } - -// /** -// * @title HTTP Request Module Unit tests -// */ -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_ContractCallRequestModule public contractCallRequestModule; -// // A mock oracle -// IOracle public oracle; -// // A mock accounting extension -// IAccountingExtension public accounting; -// // A mock user for testing -// address internal _user = makeAddr('user'); -// // A second mock user for testing -// address internal _user2 = makeAddr('user2'); -// // A mock ERC20 token -// IERC20 internal _token = IERC20(makeAddr('ERC20')); -// // Mock data -// address internal _targetContract = address(_token); -// bytes4 internal _functionSelector = bytes4(abi.encodeWithSignature('allowance(address,address)')); -// bytes internal _dataParams = abi.encode(_user, _user2); - -// event RequestFinalized(bytes32 indexed _requestId, address _finalizer); - -// /** -// * @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'); - -// contractCallRequestModule = new ForTest_ContractCallRequestModule(oracle); -// } -// } - -// contract ContractCallRequestModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(contractCallRequestModule.moduleName(), 'ContractCallRequestModule', 'Wrong module name'); -// } - -// /** -// * @notice Test that the decodeRequestData function returns the correct values -// */ -// function test_decodeRequestData(bytes32 _requestId, IERC20 _paymentToken, uint256 _paymentAmount) public { -// bytes memory _requestData = abi.encode( -// IContractCallRequestModule.RequestParameters({ -// target: _targetContract, -// functionSelector: _functionSelector, -// data: _dataParams, -// accountingExtension: accounting, -// paymentToken: _paymentToken, -// paymentAmount: _paymentAmount -// }) -// ); - -// // Set the request data -// contractCallRequestModule.forTest_setRequestData(_requestId, _requestData); - -// // Decode the given request data -// IContractCallRequestModule.RequestParameters memory _params = -// contractCallRequestModule.decodeRequestData(_requestId); - -// // Check: decoded values match original values? -// assertEq(_params.target, _targetContract, 'Mismatch: decoded target'); -// assertEq(_params.functionSelector, _functionSelector, 'Mismatch: decoded function selector'); -// assertEq(_params.data, _dataParams, 'Mismatch: decoded data'); -// assertEq(address(_params.accountingExtension), address(accounting), 'Mismatch: decoded accounting extension'); -// assertEq(address(_params.paymentToken), address(_paymentToken), 'Mismatch: decoded payment token'); -// assertEq(_params.paymentAmount, _paymentAmount, 'Mismatch: decoded payment amount'); -// } -// } - -// contract ContractCallRequestModule_Unit_Setup is BaseTest { -// /** -// * @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 { -// bytes memory _requestData = abi.encode( -// IContractCallRequestModule.RequestParameters({ -// target: _targetContract, -// functionSelector: _functionSelector, -// data: _dataParams, -// accountingExtension: accounting, -// paymentToken: _paymentToken, -// paymentAmount: _paymentAmount -// }) -// ); - -// IOracle.Request memory _fullRequest; -// _fullRequest.requester = _requester; - -// // Mock and expect IOracle.getRequest to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId)), abi.encode(_fullRequest)); - -// // Mock and expect IAccountingExtension.bond to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeWithSignature( -// 'bond(address,bytes32,address,uint256)', _requester, _requestId, _paymentToken, _paymentAmount -// ), -// abi.encode(true) -// ); - -// vm.prank(address(oracle)); -// contractCallRequestModule.setupRequest(_requestId, _requestData); - -// // Check: request data was set? -// assertEq(contractCallRequestModule.requestData(_requestId), _requestData, 'Mismatch: Request data'); -// } -// } - -// contract ContractCallRequestModule_Unit_FinalizeRequest is BaseTest { -// /** -// * @notice Test that finalizeRequest calls: -// * - oracle get request -// * - oracle get response -// * - accounting extension pay -// * - accounting extension release -// */ -// function test_makesCalls( -// bytes32 _requestId, -// address _requester, -// address _proposer, -// IERC20 _paymentToken, -// uint256 _paymentAmount -// ) public { -// // Use the correct accounting parameters -// bytes memory _requestData = abi.encode( -// IContractCallRequestModule.RequestParameters({ -// target: _targetContract, -// functionSelector: _functionSelector, -// data: _dataParams, -// accountingExtension: accounting, -// paymentToken: _paymentToken, -// paymentAmount: _paymentAmount -// }) -// ); - -// IOracle.Request memory _fullRequest; -// _fullRequest.requester = _requester; - -// IOracle.Response memory _fullResponse; -// _fullResponse.proposer = _proposer; -// _fullResponse.createdAt = block.timestamp; - -// // Set the request data -// contractCallRequestModule.forTest_setRequestData(_requestId, _requestData); - -// // Mock and expect IOracle.getRequest to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId)), abi.encode(_fullRequest)); -// vm.expectCall(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId))); - -// // Mock and expect IOracle.getFinalizedResponse to be called -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse) -// ); - -// // Mock and expect IAccountingExtension.pay to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _paymentToken, _paymentAmount)), -// abi.encode() -// ); - -// vm.startPrank(address(oracle)); -// contractCallRequestModule.finalizeRequest(_requestId, address(oracle)); - -// // Test the release flow -// _fullResponse.createdAt = 0; - -// // Update mock call to return the response with createdAt = 0 -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse) -// ); - -// // Mock and expect IAccountingExtension.release to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _paymentToken, _paymentAmount)), -// abi.encode(true) -// ); - -// contractCallRequestModule.finalizeRequest(_requestId, address(this)); -// } - -// function test_emitsEvent( -// bytes32 _requestId, -// address _requester, -// address _proposer, -// IERC20 _paymentToken, -// uint256 _paymentAmount -// ) public { -// // Use the correct accounting parameters -// bytes memory _requestData = abi.encode( -// IContractCallRequestModule.RequestParameters({ -// target: _targetContract, -// functionSelector: _functionSelector, -// data: _dataParams, -// accountingExtension: accounting, -// paymentToken: _paymentToken, -// paymentAmount: _paymentAmount -// }) -// ); - -// IOracle.Request memory _fullRequest; -// _fullRequest.requester = _requester; - -// IOracle.Response memory _fullResponse; -// _fullResponse.proposer = _proposer; -// _fullResponse.createdAt = block.timestamp; - -// // Set the request data -// contractCallRequestModule.forTest_setRequestData(_requestId, _requestData); - -// // Mock and expect IOracle.getRequest to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId)), abi.encode(_fullRequest)); - -// // Mock and expect IAccountingExtension.pay to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _paymentToken, _paymentAmount)), -// abi.encode() -// ); - -// // Mock and expect IOracle.getFinalizedResponse to be called -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse) -// ); - -// vm.startPrank(address(oracle)); -// contractCallRequestModule.finalizeRequest(_requestId, address(oracle)); - -// // Test the release flow -// _fullResponse.createdAt = 0; - -// // Update mock call to return the response with createdAt = 0 -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse) -// ); - -// // Mock and expect IAccountingExtension.release to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _paymentToken, _paymentAmount)), -// abi.encode(true) -// ); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(contractCallRequestModule)); -// emit RequestFinalized(_requestId, address(this)); - -// contractCallRequestModule.finalizeRequest(_requestId, address(this)); -// } - -// /** -// * @notice Test that the finalizeRequest reverts if caller is not the oracle -// */ -// function test_revertsIfWrongCaller(bytes32 _requestId, address _caller) public { -// vm.assume(_caller != address(oracle)); - -// // Check: does it revert if not called by the Oracle? -// vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); - -// vm.prank(_caller); -// contractCallRequestModule.finalizeRequest(_requestId, address(_caller)); -// } -// } +// 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 { + ContractCallRequestModule, + IContractCallRequestModule +} from '../../../../contracts/modules/request/ContractCallRequestModule.sol'; + +import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; + +/** + * @title Contract Call Request Module Unit tests + */ +contract BaseTest is Test, Helpers { + // The target contract + ContractCallRequestModule public contractCallRequestModule; + // A mock oracle + IOracle public oracle; + // A mock accounting extension + IAccountingExtension public accounting; + // A mock user for testing + address internal _user = makeAddr('user'); + // A second mock user for testing + address internal _user2 = makeAddr('user2'); + // A mock ERC20 token + IERC20 internal _token = IERC20(makeAddr('ERC20')); + // Mock data + address internal _targetContract = address(_token); + bytes4 internal _functionSelector = bytes4(abi.encodeWithSignature('allowance(address,address)')); + bytes internal _dataParams = abi.encode(_user, _user2); + + /** + * @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'); + + contractCallRequestModule = new ContractCallRequestModule(oracle); + } +} + +contract ContractCallRequestModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(contractCallRequestModule.moduleName(), 'ContractCallRequestModule', 'Wrong module name'); + } + + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData(IERC20 _paymentToken, uint256 _paymentAmount) public { + bytes memory _requestData = abi.encode( + IContractCallRequestModule.RequestParameters({ + target: _targetContract, + functionSelector: _functionSelector, + data: _dataParams, + accountingExtension: accounting, + paymentToken: _paymentToken, + paymentAmount: _paymentAmount + }) + ); + + // Decode the given request data + IContractCallRequestModule.RequestParameters memory _params = + contractCallRequestModule.decodeRequestData(_requestData); + + // Check: decoded values match original values? + assertEq(_params.target, _targetContract, 'Mismatch: decoded target'); + assertEq(_params.functionSelector, _functionSelector, 'Mismatch: decoded function selector'); + assertEq(_params.data, _dataParams, 'Mismatch: decoded data'); + assertEq(address(_params.accountingExtension), address(accounting), 'Mismatch: decoded accounting extension'); + assertEq(address(_params.paymentToken), address(_paymentToken), 'Mismatch: decoded payment token'); + assertEq(_params.paymentAmount, _paymentAmount, 'Mismatch: decoded payment amount'); + } +} + +contract ContractCallRequestModule_Unit_FinalizeRequest is BaseTest { + /** + * @notice Test that finalizeRequest calls: + * - oracle get request + * - oracle get response + * - accounting extension pay + * - accounting extension release + */ + function test_finalizeWithResponse(IERC20 _paymentToken, uint256 _paymentAmount) public { + mockRequest.requestModuleData = abi.encode( + IContractCallRequestModule.RequestParameters({ + target: _targetContract, + functionSelector: _functionSelector, + data: _dataParams, + accountingExtension: accounting, + paymentToken: _paymentToken, + paymentAmount: _paymentAmount + }) + ); + + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + + // Mock and expect oracle to return the response's creation time + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.createdAt, (_getId(mockResponse))), abi.encode(block.timestamp) + ); + + // Mock and expect IAccountingExtension.pay to be called + _mockAndExpect( + address(accounting), + abi.encodeCall( + IAccountingExtension.pay, + (_requestId, mockRequest.requester, mockResponse.proposer, _paymentToken, _paymentAmount) + ), + abi.encode() + ); + + vm.startPrank(address(oracle)); + contractCallRequestModule.finalizeRequest(mockRequest, mockResponse, address(oracle)); + } + + function test_finalizeWithoutResponse(IERC20 _paymentToken, uint256 _paymentAmount) public { + mockRequest.requestModuleData = abi.encode( + IContractCallRequestModule.RequestParameters({ + target: _targetContract, + functionSelector: _functionSelector, + data: _dataParams, + accountingExtension: accounting, + paymentToken: _paymentToken, + paymentAmount: _paymentAmount + }) + ); + + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + + // Mock and expect oracle to return no timestamp + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.createdAt, (_getId(mockResponse))), abi.encode(0)); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (mockRequest.requester, _requestId, _paymentToken, _paymentAmount)), + abi.encode(true) + ); + + vm.startPrank(address(oracle)); + contractCallRequestModule.finalizeRequest(mockRequest, mockResponse, address(oracle)); + } + + function test_emitsEvent(IERC20 _paymentToken, uint256 _paymentAmount) public { + // Use the correct accounting parameters + mockRequest.requestModuleData = abi.encode( + IContractCallRequestModule.RequestParameters({ + target: _targetContract, + functionSelector: _functionSelector, + data: _dataParams, + accountingExtension: accounting, + paymentToken: _paymentToken, + paymentAmount: _paymentAmount + }) + ); + + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + + // Mock and expect oracle to return no timestamp + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.createdAt, (_getId(mockResponse))), abi.encode(0)); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (mockRequest.requester, _requestId, _paymentToken, _paymentAmount)), + abi.encode(true) + ); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(contractCallRequestModule)); + emit RequestFinalized(_requestId, mockResponse, address(this)); + + vm.prank(address(oracle)); + contractCallRequestModule.finalizeRequest(mockRequest, mockResponse, address(this)); + } + + /** + * @notice Test that the finalizeRequest reverts if caller is not the oracle + */ + function test_revertsIfWrongCaller(address _caller, IOracle.Request calldata _request) public { + vm.assume(_caller != address(oracle)); + + // Check: does it revert if not called by the Oracle? + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + + vm.prank(_caller); + contractCallRequestModule.finalizeRequest(_request, mockResponse, address(_caller)); + } +}