-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: contract call request module (#38)
- Loading branch information
1 parent
eb8fc22
commit 64daadc
Showing
3 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.19; | ||
|
||
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; | ||
|
||
import {IContractCallRequestModule} from '../../interfaces/modules/IContractCallRequestModule.sol'; | ||
import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; | ||
import {IOracle} from '../../interfaces/IOracle.sol'; | ||
import {IModule, Module} from '../Module.sol'; | ||
|
||
contract ContractCallRequestModule is Module, IContractCallRequestModule { | ||
constructor(IOracle _oracle) Module(_oracle) {} | ||
|
||
function decodeRequestData(bytes32 _requestId) | ||
external | ||
view | ||
returns ( | ||
address _target, | ||
bytes4 _functionSelector, | ||
bytes memory _data, | ||
IAccountingExtension _accountingExtension, | ||
IERC20 _paymentToken, | ||
uint256 _paymentAmount | ||
) | ||
{ | ||
(_target, _functionSelector, _data, _accountingExtension, _paymentToken, _paymentAmount) = | ||
_decodeRequestData(requestData[_requestId]); | ||
} | ||
|
||
function _decodeRequestData(bytes memory _encodedData) | ||
internal | ||
pure | ||
returns ( | ||
address _target, | ||
bytes4 _functionSelector, | ||
bytes memory _data, | ||
IAccountingExtension _accountingExtension, | ||
IERC20 _paymentToken, | ||
uint256 _paymentAmount | ||
) | ||
{ | ||
(_target, _functionSelector, _data, _accountingExtension, _paymentToken, _paymentAmount) = | ||
abi.decode(_encodedData, (address, bytes4, bytes, IAccountingExtension, IERC20, uint256)); | ||
} | ||
|
||
function _afterSetupRequest(bytes32 _requestId, bytes calldata _data) internal override { | ||
(,,, IAccountingExtension _accountingExtension, IERC20 _paymentToken, uint256 _paymentAmount) = | ||
_decodeRequestData(_data); | ||
IOracle.Request memory _request = ORACLE.getRequest(_requestId); | ||
_accountingExtension.bond(_request.requester, _requestId, _paymentToken, _paymentAmount); | ||
} | ||
|
||
function finalizeRequest(bytes32 _requestId) 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(requestData[_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 = 'ContractCallRequestModule'; | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
solidity/interfaces/modules/IContractCallRequestModule.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// 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 IContractCallRequestModule is IRequestModule { | ||
function decodeRequestData(bytes32 _requestId) | ||
external | ||
view | ||
returns ( | ||
address _target, | ||
bytes4 _functionSelector, | ||
bytes memory _data, | ||
IAccountingExtension _accountingExtension, | ||
IERC20 _paymentToken, | ||
uint256 _paymentAmount | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
pragma solidity ^0.8.19; | ||
|
||
// solhint-disable-next-line | ||
import 'forge-std/Test.sol'; | ||
|
||
import { | ||
ContractCallRequestModule, | ||
IModule, | ||
IOracle, | ||
IAccountingExtension, | ||
IERC20 | ||
} from '../../contracts/modules/ContractCallRequestModule.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 ContractCallRequestModule_UnitTest is Test { | ||
// 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 _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 _targetContract = address(_token); | ||
bytes4 _functionSelector = bytes4(abi.encodeWithSignature('allowance(address, address)')); | ||
bytes _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 ForTest_ContractCallRequestModule(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(_targetContract, _functionSelector, _dataParams, accounting, _paymentToken, _paymentAmount); | ||
|
||
// Set the request data | ||
contractCallRequestModule.forTest_setRequestData(_requestId, _requestData); | ||
|
||
// Decode the given request data | ||
( | ||
address _decodedTarget, | ||
bytes4 _decodedFunctionSelector, | ||
bytes memory _decodedData, | ||
IAccountingExtension _decodedAccountingExtension, | ||
IERC20 _decodedPaymentToken, | ||
uint256 _decodedPaymentAmount | ||
) = contractCallRequestModule.decodeRequestData(_requestId); | ||
|
||
// Check: decoded values match original values? | ||
assertEq(_decodedTarget, _targetContract, 'Mismatch: decoded target'); | ||
assertEq(_decodedFunctionSelector, _functionSelector, 'Mismatch: decoded function selector'); | ||
assertEq(_decodedData, _dataParams, 'Mismatch: decoded data'); | ||
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(_targetContract, _functionSelector, _dataParams, 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)) | ||
); | ||
|
||
contractCallRequestModule.setupRequest(_requestId, _requestData); | ||
|
||
// Check: request data was set? | ||
assertEq(contractCallRequestModule.requestData(_requestId), _requestData, 'Mismatch: Request data'); | ||
} | ||
|
||
/** | ||
* @notice Test that the moduleName function returns the correct name | ||
*/ | ||
function test_moduleNameReturnsName() public { | ||
assertEq(contractCallRequestModule.moduleName(), 'ContractCallRequestModule', '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(_targetContract, _functionSelector, _dataParams, 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 | ||
contractCallRequestModule.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)); | ||
contractCallRequestModule.finalizeRequest(_requestId); | ||
|
||
// 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)) | ||
); | ||
|
||
contractCallRequestModule.finalizeRequest(_requestId); | ||
} | ||
|
||
/** | ||
* @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)); | ||
contractCallRequestModule.finalizeRequest(_requestId); | ||
} | ||
} |