Skip to content

Commit

Permalink
feat: contract call request module (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
turtlemoji authored Jul 5, 2023
1 parent eb8fc22 commit 64daadc
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 0 deletions.
68 changes: 68 additions & 0 deletions solidity/contracts/modules/ContractCallRequestModule.sol
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 solidity/interfaces/modules/IContractCallRequestModule.sol
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
);
}
233 changes: 233 additions & 0 deletions solidity/test/unit/ContractCallRequestModule.t.sol
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);
}
}

0 comments on commit 64daadc

Please sign in to comment.