From c18b18345c3281dadf229843cba5d331d0cc7508 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Fri, 3 Nov 2023 16:34:47 +0400 Subject: [PATCH 01/53] feat: update `BondedDisputeModule` --- .../modules/dispute/bonded_dispute_module.md | 2 +- .../modules/dispute/BondedDisputeModule.sol | 196 +++-- .../modules/dispute/IBondedDisputeModule.sol | 131 ++-- .../modules/dispute/BondedDisputeModule.t.sol | 734 ++++++++---------- solidity/test/utils/Helpers.sol | 96 ++- 5 files changed, 532 insertions(+), 627 deletions(-) diff --git a/docs/src/content/modules/dispute/bonded_dispute_module.md b/docs/src/content/modules/dispute/bonded_dispute_module.md index b86745c2..94d9dda3 100644 --- a/docs/src/content/modules/dispute/bonded_dispute_module.md +++ b/docs/src/content/modules/dispute/bonded_dispute_module.md @@ -10,7 +10,7 @@ The Bonded Dispute Module is a contract that allows users to dispute a proposed ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: Returns the decoded data for a request. +- `decodeRequestData(bytes calldata _data)`: Returns the decoded data for a request. - `disputeResponse(bytes32 _requestId, bytes32 _responseId, address _disputer, address _proposer)`: Starts a dispute. - `onDisputeStatusChange(bytes32 _disputeId, IOracle.Dispute memory _dispute)`: Is a hook called by the oracle when a dispute status has been updated. - `disputeEscalated(bytes32 _disputeId)`: Called by the oracle when a dispute has been escalated. Not implemented in this module. diff --git a/solidity/contracts/modules/dispute/BondedDisputeModule.sol b/solidity/contracts/modules/dispute/BondedDisputeModule.sol index 8fb44575..bc03ebf9 100644 --- a/solidity/contracts/modules/dispute/BondedDisputeModule.sol +++ b/solidity/contracts/modules/dispute/BondedDisputeModule.sol @@ -1,115 +1,105 @@ -// // 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 {IBondedDisputeModule} from '../../../interfaces/modules/dispute/IBondedDisputeModule.sol'; +import {IBondedDisputeModule} from '../../../interfaces/modules/dispute/IBondedDisputeModule.sol'; -// contract BondedDisputeModule is Module, IBondedDisputeModule { -// constructor(IOracle _oracle) Module(_oracle) {} +contract BondedDisputeModule is Module, IBondedDisputeModule { + constructor(IOracle _oracle) Module(_oracle) {} -// /// @inheritdoc IModule -// function moduleName() external pure returns (string memory _moduleName) { -// return 'BondedDisputeModule'; -// } + /// @inheritdoc IModule + function moduleName() external pure returns (string memory _moduleName) { + return 'BondedDisputeModule'; + } -// /// @inheritdoc IBondedDisputeModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } + /// @inheritdoc IBondedDisputeModule + function decodeRequestData(bytes calldata _data) public view returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } -// /// @inheritdoc IBondedDisputeModule -// function disputeEscalated(bytes32 _disputeId) external onlyOracle {} + /// @inheritdoc IBondedDisputeModule + function disputeResponse( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.disputeModuleData); -// /// @inheritdoc IBondedDisputeModule -// function disputeResponse( -// bytes32 _requestId, -// bytes32 _responseId, -// address _disputer, -// address _proposer -// ) external onlyOracle returns (IOracle.Dispute memory _dispute) { -// _dispute = IOracle.Dispute({ -// disputer: _disputer, -// responseId: _responseId, -// proposer: _proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.Active, -// createdAt: block.timestamp -// }); + _params.accountingExtension.bond({ + _bonder: _dispute.disputer, + _requestId: _dispute.requestId, + _token: _params.bondToken, + _amount: _params.bondSize + }); -// RequestParameters memory _params = decodeRequestData(_requestId); -// _params.accountingExtension.bond({ -// _bonder: _disputer, -// _requestId: _requestId, -// _token: _params.bondToken, -// _amount: _params.bondSize -// }); + emit ResponseDisputed({ + _requestId: _dispute.requestId, + _responseId: _dispute.responseId, + _dispute: _dispute, + blockNumber: block.number + }); + } -// emit ResponseDisputed(_requestId, _responseId, _disputer, _proposer); -// } + /// @inheritdoc IBondedDisputeModule + function onDisputeStatusChange( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.disputeModuleData); + IOracle.DisputeStatus _status = ORACLE.disputeStatus(_disputeId); -// /// @inheritdoc IBondedDisputeModule -// function onDisputeStatusChange(bytes32, /* _disputeId */ IOracle.Dispute memory _dispute) external onlyOracle { -// RequestParameters memory _params = decodeRequestData(_dispute.requestId); -// IOracle.DisputeStatus _status = _dispute.status; -// address _proposer = _dispute.proposer; -// address _disputer = _dispute.disputer; + if (_status == IOracle.DisputeStatus.NoResolution) { + // No resolution, we release both bonds + _params.accountingExtension.release({ + _bonder: _dispute.disputer, + _requestId: _dispute.requestId, + _token: _params.bondToken, + _amount: _params.bondSize + }); -// if (_status == IOracle.DisputeStatus.NoResolution) { -// // No resolution, we release both bonds -// _params.accountingExtension.release({ -// _bonder: _disputer, -// _requestId: _dispute.requestId, -// _token: _params.bondToken, -// _amount: _params.bondSize -// }); + _params.accountingExtension.release({ + _bonder: _dispute.proposer, + _requestId: _dispute.requestId, + _token: _params.bondToken, + _amount: _params.bondSize + }); + } else if (_status == IOracle.DisputeStatus.Won) { + // Disputer won, we pay the disputer and release their bond + _params.accountingExtension.pay({ + _requestId: _dispute.requestId, + _payer: _dispute.proposer, + _receiver: _dispute.disputer, + _token: _params.bondToken, + _amount: _params.bondSize + }); + _params.accountingExtension.release({ + _bonder: _dispute.disputer, + _requestId: _dispute.requestId, + _token: _params.bondToken, + _amount: _params.bondSize + }); + } else if (_status == IOracle.DisputeStatus.Lost) { + // Disputer lost, we pay the proposer and release their bond + _params.accountingExtension.pay({ + _requestId: _dispute.requestId, + _payer: _dispute.disputer, + _receiver: _dispute.proposer, + _token: _params.bondToken, + _amount: _params.bondSize + }); + _params.accountingExtension.release({ + _bonder: _dispute.proposer, + _requestId: _dispute.requestId, + _token: _params.bondToken, + _amount: _params.bondSize + }); + } -// _params.accountingExtension.release({ -// _bonder: _proposer, -// _requestId: _dispute.requestId, -// _token: _params.bondToken, -// _amount: _params.bondSize -// }); -// } else if (_status == IOracle.DisputeStatus.Won) { -// // Disputer won, we pay the disputer and release their bond -// _params.accountingExtension.pay({ -// _requestId: _dispute.requestId, -// _payer: _proposer, -// _receiver: _disputer, -// _token: _params.bondToken, -// _amount: _params.bondSize -// }); -// _params.accountingExtension.release({ -// _bonder: _disputer, -// _requestId: _dispute.requestId, -// _token: _params.bondToken, -// _amount: _params.bondSize -// }); -// } else if (_status == IOracle.DisputeStatus.Lost) { -// // Disputer lost, we pay the proposer and release their bond -// _params.accountingExtension.pay({ -// _requestId: _dispute.requestId, -// _payer: _disputer, -// _receiver: _proposer, -// _token: _params.bondToken, -// _amount: _params.bondSize -// }); -// _params.accountingExtension.release({ -// _bonder: _proposer, -// _requestId: _dispute.requestId, -// _token: _params.bondToken, -// _amount: _params.bondSize -// }); -// } - -// emit DisputeStatusChanged({ -// _requestId: _dispute.requestId, -// _responseId: _dispute.responseId, -// _disputer: _disputer, -// _proposer: _proposer, -// _status: _status -// }); -// } -// } + emit DisputeStatusChanged({_disputeId: _disputeId, _dispute: _dispute, _status: _status}); + } +} diff --git a/solidity/interfaces/modules/dispute/IBondedDisputeModule.sol b/solidity/interfaces/modules/dispute/IBondedDisputeModule.sol index 164e0a29..db3969aa 100644 --- a/solidity/interfaces/modules/dispute/IBondedDisputeModule.sol +++ b/solidity/interfaces/modules/dispute/IBondedDisputeModule.sol @@ -1,75 +1,72 @@ -// // 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 BondedDisputeModule -// * @notice Module allowing users to dispute a proposed response -// * by bonding tokens. According to the result of the dispute, -// * the tokens are either returned to the disputer or to the proposer. -// */ -// interface IBondedDisputeModule is IDisputeModule { -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ +/* + * @title BondedDisputeModule + * @notice Module allowing users to dispute a proposed response + * by bonding tokens. According to the result of the dispute, + * the tokens are either returned to the disputer or to the proposer. + */ +interface IBondedDisputeModule is IDisputeModule { + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Parameters of the request as stored in the module -// * @param ipfsHash The hash of the CID from IPFS -// * @param requestModule The address of the request module -// * @param responseModule The address of the response module -// */ -// struct RequestParameters { -// IAccountingExtension accountingExtension; -// IERC20 bondToken; -// uint256 bondSize; -// } + /** + * @notice Parameters of the request as stored in the module + * @param ipfsHash The hash of the CID from IPFS + * @param requestModule The address of the request module + * @param responseModule The address of the response module + */ + struct RequestParameters { + IAccountingExtension accountingExtension; + IERC20 bondToken; + uint256 bondSize; + } -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ -// /** -// * @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 Called by the oracle when a dispute has been made on a response. -// * Bonds the tokens of the disputer. -// * @param _requestId The ID of the request whose response is disputed -// * @param _responseId The ID of the response being disputed -// * @param _disputer The address of the user who disputed the response -// * @param _proposer The address of the user who proposed the disputed response -// * @return _dispute The dispute on the proposed response -// */ -// function disputeResponse( -// bytes32 _requestId, -// bytes32 _responseId, -// address _disputer, -// address _proposer -// ) external returns (IOracle.Dispute memory _dispute); + /** + * @notice Called by the oracle when a dispute has been made on a response + * + * @dev Bonds the tokens of the disputer + * @param _request The request a dispute has been submitted for + * @param _response The response that is being disputed + * @param _dispute The dispute that is being submitted + */ + function disputeResponse( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; -// /** -// * @notice Called by the oracle when a dispute status has been updated. -// * According to the result of the dispute, bonds are released to the proposer or -// * paid to the disputer. -// * @param _disputeId The ID of the dispute being updated -// * @param _dispute The dispute object -// */ -// function onDisputeStatusChange(bytes32 _disputeId, IOracle.Dispute memory _dispute) external; - -// /** -// * @notice Called by the oracle when a dispute has been escalated. Not implemented in this module -// * @param _disputeId The ID of the dispute being escalated -// */ -// function disputeEscalated(bytes32 _disputeId) external; -// } + /** + * @notice Called by the oracle when a dispute status has been updated + * + * @dev According to the result of the dispute, bonds are released to the proposer or paid to the disputer + * @param _disputeId The ID of the dispute being updated + * @param _dispute The dispute object + */ + function onDisputeStatusChange( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; +} diff --git a/solidity/test/unit/modules/dispute/BondedDisputeModule.t.sol b/solidity/test/unit/modules/dispute/BondedDisputeModule.t.sol index 3ad054d0..b02c87ef 100644 --- a/solidity/test/unit/modules/dispute/BondedDisputeModule.t.sol +++ b/solidity/test/unit/modules/dispute/BondedDisputeModule.t.sol @@ -1,407 +1,327 @@ -// // 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 { -// BondedDisputeModule, IBondedDisputeModule -// } from '../../../../contracts/modules/dispute/BondedDisputeModule.sol'; - -// import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; - -// contract ForTest_BondedDisputeModule is BondedDisputeModule { -// constructor(IOracle _oracle) BondedDisputeModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } -// } - -// /** -// * @title Bonded Dispute Module Unit tests -// */ -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_BondedDisputeModule public bondedDisputeModule; -// // A mock accounting extension -// IAccountingExtension public accountingExtension; -// // A mock oracle -// IOracle public oracle; -// // 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; - -// event DisputeStatusChanged( -// bytes32 indexed _requestId, bytes32 _responseId, address _disputer, address _proposer, IOracle.DisputeStatus _status -// ); - -// /** -// * @notice Deploy the target and mock oracle -// */ -// function setUp() public { -// oracle = IOracle(makeAddr('Oracle')); -// vm.etch(address(oracle), hex'069420'); - -// accountingExtension = IAccountingExtension(makeAddr('AccountingExtension')); -// vm.etch(address(accountingExtension), hex'069420'); - -// bondedDisputeModule = new ForTest_BondedDisputeModule(oracle); - -// mockDispute = IOracle.Dispute({ -// createdAt: block.timestamp, -// disputer: dude, -// proposer: dude, -// responseId: mockId, -// requestId: mockId, -// status: IOracle.DisputeStatus.Active -// }); -// } -// } - -// contract BondedResponseModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the decodeRequestData function returns the correct values -// */ -// function test_decodeRequestData_returnsCorrectData( -// bytes32 _requestId, -// address _accountingExtension, -// address _token, -// uint256 _bondSize -// ) public { -// // Mock data -// bytes memory _requestData = abi.encode(_accountingExtension, _token, _bondSize); - -// // Store the mock request -// bondedDisputeModule.forTest_setRequestData(_requestId, _requestData); - -// // Test: decode the given request data -// IBondedDisputeModule.RequestParameters memory _storedParams = bondedDisputeModule.decodeRequestData(_requestId); - -// // Check: decoded values match original values? -// assertEq(address(_storedParams.accountingExtension), _accountingExtension); -// assertEq(address(_storedParams.bondToken), _token); -// assertEq(_storedParams.bondSize, _bondSize); -// } - -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(bondedDisputeModule.moduleName(), 'BondedDisputeModule'); -// } -// } - -// contract BondedResponseModule_Unit_OnDisputeStatusChange is BaseTest { -// /** -// * @notice Test if onDisputeStatusChange correctly handle proposer or disputer win -// */ -// function test_correctWinnerPaid(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { -// // Mock id's (insure they are different) -// bytes32 _requestId = mockId; -// bytes32 _responseId = bytes32(uint256(mockId) + 1); - -// // Mock request data -// bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); - -// // Store the mock request -// bondedDisputeModule.forTest_setRequestData(mockId, _requestData); - -// // ------------------------------------ -// // Scenario: dispute won by proposer -// // ------------------------------------ - -// mockDispute = IOracle.Dispute({ -// createdAt: 1, -// disputer: _disputer, -// proposer: _proposer, -// responseId: _responseId, -// requestId: _requestId, -// status: IOracle.DisputeStatus.Won -// }); - -// // Mock and expect the call to pay, from¨*proposer to disputer* -// _mockAndExpect( -// address(accountingExtension), -// abi.encodeCall(accountingExtension.pay, (_requestId, _proposer, _disputer, _token, _bondSize)), -// abi.encode() -// ); - -// // Mock and expect the call to release, to the disputer -// _mockAndExpect( -// address(accountingExtension), -// abi.encodeCall(accountingExtension.release, (_disputer, _requestId, _token, _bondSize)), -// abi.encode() -// ); - -// vm.prank(address(oracle)); -// bondedDisputeModule.onDisputeStatusChange(mockId, mockDispute); - -// // ------------------------------------ -// // Scenario: dispute loss by proposer -// // ------------------------------------ - -// mockDispute = IOracle.Dispute({ -// createdAt: 1, -// disputer: _disputer, -// proposer: _proposer, -// responseId: _responseId, -// requestId: _requestId, -// status: IOracle.DisputeStatus.Lost -// }); - -// // Mock and expect the call to pay, from *disputer to proposer* -// _mockAndExpect( -// address(accountingExtension), -// abi.encodeCall(accountingExtension.pay, (_requestId, _disputer, _proposer, _token, _bondSize)), -// abi.encode() -// ); - -// // Mock and expect the call to release, for the proposer -// _mockAndExpect( -// address(accountingExtension), -// abi.encodeCall(accountingExtension.release, (_proposer, _requestId, _token, _bondSize)), -// abi.encode() -// ); - -// vm.prank(address(oracle)); -// bondedDisputeModule.onDisputeStatusChange(mockId, mockDispute); - -// // ------------------------------------ -// // Scenario: dispute with no resolution -// // ------------------------------------ - -// mockDispute = IOracle.Dispute({ -// createdAt: 1, -// disputer: _disputer, -// proposer: _proposer, -// responseId: _responseId, -// requestId: _requestId, -// status: IOracle.DisputeStatus.NoResolution -// }); - -// // Mock and expect the call to release, for the proposer -// _mockAndExpect( -// address(accountingExtension), -// abi.encodeCall(accountingExtension.release, (_proposer, _requestId, _token, _bondSize)), -// abi.encode() -// ); - -// // Mock and expect the call to release, for the disputer -// _mockAndExpect( -// address(accountingExtension), -// abi.encodeCall(accountingExtension.release, (_disputer, _requestId, _token, _bondSize)), -// abi.encode() -// ); - -// vm.prank(address(oracle)); -// bondedDisputeModule.onDisputeStatusChange(mockId, mockDispute); -// } - -// function test_statusWithNoChange(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { -// // Mock id's (insure they are different) -// bytes32 _requestId = mockId; -// bytes32 _responseId = bytes32(uint256(mockId) + 1); - -// // Mock request data -// bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); - -// // Store the mock request -// bondedDisputeModule.forTest_setRequestData(mockId, _requestData); - -// // ------------------------------------ -// // Scenario: dispute new status is None -// // ------------------------------------ - -// mockDispute = IOracle.Dispute({ -// createdAt: 1, -// disputer: _disputer, -// proposer: _proposer, -// responseId: _responseId, -// requestId: _requestId, -// status: IOracle.DisputeStatus.None -// }); - -// // Expect the event -// vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); -// emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.None); - -// vm.prank(address(oracle)); -// bondedDisputeModule.onDisputeStatusChange(mockId, mockDispute); - -// // ------------------------------------ -// // Scenario: dispute new status is Active -// // ------------------------------------ - -// mockDispute = IOracle.Dispute({ -// createdAt: 1, -// disputer: _disputer, -// proposer: _proposer, -// responseId: _responseId, -// requestId: _requestId, -// status: IOracle.DisputeStatus.Active -// }); - -// // Expect the event -// vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); -// emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.Active); - -// vm.prank(address(oracle)); -// bondedDisputeModule.onDisputeStatusChange(mockId, mockDispute); -// // ------------------------------------ -// // Scenario: dispute new status is Escalated -// // ------------------------------------ - -// mockDispute = IOracle.Dispute({ -// createdAt: 1, -// disputer: _disputer, -// proposer: _proposer, -// responseId: _responseId, -// requestId: _requestId, -// status: IOracle.DisputeStatus.Escalated -// }); - -// // Expect the event -// vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); -// emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.Escalated); - -// vm.prank(address(oracle)); -// bondedDisputeModule.onDisputeStatusChange(mockId, mockDispute); -// } - -// function test_emitsEvent(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { -// // Mock id's (insure they are different) -// bytes32 _requestId = mockId; -// bytes32 _responseId = bytes32(uint256(mockId) + 1); - -// // Mock request data -// bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); - -// // Store the mock request -// bondedDisputeModule.forTest_setRequestData(mockId, _requestData); - -// // ------------------------------------ -// // Scenario: dispute won by proposer -// // ------------------------------------ - -// mockDispute = IOracle.Dispute({ -// createdAt: 1, -// disputer: _disputer, -// proposer: _proposer, -// responseId: _responseId, -// requestId: _requestId, -// status: IOracle.DisputeStatus.Won -// }); - -// // Mock and expect the call to pay, from¨*proposer to disputer* -// _mockAndExpect( -// address(accountingExtension), -// abi.encodeCall(accountingExtension.pay, (_requestId, _proposer, _disputer, _token, _bondSize)), -// abi.encode() -// ); - -// // Mock and expect the call to release, to the disputer -// _mockAndExpect( -// address(accountingExtension), -// abi.encodeCall(accountingExtension.release, (_disputer, _requestId, _token, _bondSize)), -// abi.encode() -// ); - -// // Expect the event -// vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); -// emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.Won); - -// vm.prank(address(oracle)); -// bondedDisputeModule.onDisputeStatusChange(mockId, mockDispute); -// } - -// /** -// * @notice Test if onDisputeStatusChange reverts when called by caller who's not the oracle -// */ -// function test_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); -// bondedDisputeModule.onDisputeStatusChange(mockId, mockDispute); -// } -// } - -// contract BondedResponseModule_Unit_DisputeResponse is BaseTest { -// /** -// * @notice Test if dispute escalated do nothing -// */ -// function test_returnCorrectStatus() public { -// // Record sstore and sload -// vm.prank(address(oracle)); -// vm.record(); -// bondedDisputeModule.disputeEscalated(mockId); -// (bytes32[] memory _reads, bytes32[] memory _writes) = vm.accesses(address(bondedDisputeModule)); - -// // Check: no storage access? -// assertEq(_reads.length, 0); -// assertEq(_writes.length, 0); -// } - -// /** -// * @notice Test if dispute response returns the correct status -// */ -// function test_createBond(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { -// // Mock id's (insure they are different) -// bytes32 _requestId = mockId; -// bytes32 _responseId = bytes32(uint256(mockId) + 1); - -// // Mock request data -// bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); - -// // Store the mock request -// bondedDisputeModule.forTest_setRequestData(mockId, _requestData); - -// // Mock and expect the call to the accounting extension, initiating the bond -// _mockAndExpect( -// address(accountingExtension), -// abi.encodeWithSignature('bond(address,bytes32,address,uint256)', _disputer, _requestId, _token, _bondSize), -// abi.encode() -// ); - -// // Test: call disputeResponse -// vm.prank(address(oracle)); -// IOracle.Dispute memory _dispute = bondedDisputeModule.disputeResponse(_requestId, _responseId, _disputer, _proposer); - -// // Check: dispute is correct? -// assertEq(_dispute.disputer, _disputer); -// assertEq(_dispute.proposer, _proposer); -// assertEq(_dispute.responseId, _responseId); -// assertEq(_dispute.requestId, _requestId); -// assertEq(uint256(_dispute.status), uint256(IOracle.DisputeStatus.Active)); -// assertEq(_dispute.createdAt, block.timestamp); -// } - -// /** -// * @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: revert if wrong caller -// vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); - -// // Test: call disputeResponse from non-oracle address -// vm.prank(_randomCaller); -// bondedDisputeModule.disputeResponse(mockId, mockId, dude, dude); -// } -// } -// /** -// * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks -// */ +// 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 { + BondedDisputeModule, IBondedDisputeModule +} from '../../../../contracts/modules/dispute/BondedDisputeModule.sol'; + +import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; + +/** + * @title Bonded Dispute Module Unit tests + */ +contract BaseTest is Test, Helpers { + // The target contract + BondedDisputeModule public bondedDisputeModule; + // A mock accounting extension + IAccountingExtension public accountingExtension; + // A mock oracle + IOracle public oracle; + // 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 dummy request + IOracle.Request public mockRequest; + // Create a dummy response + IOracle.Response public mockResponse; + // Create a dummy dispute + IOracle.Dispute public mockDispute; + + event DisputeStatusChanged(bytes32 indexed _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); + // TODO: event ResponseDisputed(bytes32 indexed _requestId, bytes32 indexed _responseId, IOracle.Dispute _dispute, uint256 _blockNumber); + + /** + * @notice Deploy the target and mock oracle + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + accountingExtension = IAccountingExtension(makeAddr('AccountingExtension')); + vm.etch(address(accountingExtension), hex'069420'); + + bondedDisputeModule = new BondedDisputeModule(oracle); + + mockDispute = IOracle.Dispute({disputer: dude, proposer: dude, responseId: mockId, requestId: mockId}); + } +} + +contract BondedResponseModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData_returnsCorrectData( + bytes32 _requestId, + address _accountingExtension, + address _token, + uint256 _bondSize + ) public { + // Mock data + bytes memory _requestData = abi.encode(_accountingExtension, _token, _bondSize); + + // Store the mock request + // bondedDisputeModule.forTest_setRequestData(_requestId, _requestData); + + // Test: decode the given request data + IBondedDisputeModule.RequestParameters memory _storedParams = bondedDisputeModule.decodeRequestData(_requestData); + + // Check: decoded values match original values? + assertEq(address(_storedParams.accountingExtension), _accountingExtension); + assertEq(address(_storedParams.bondToken), _token); + assertEq(_storedParams.bondSize, _bondSize); + } + + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(bondedDisputeModule.moduleName(), 'BondedDisputeModule'); + } +} + +contract BondedResponseModule_Unit_OnDisputeStatusChange is BaseTest { + /** + * @notice Test if onDisputeStatusChange correctly handle proposer or disputer win + */ + function test_correctWinnerPaid(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { + // Mock id's (insure they are different) + bytes32 _requestId = mockId; + bytes32 _responseId = bytes32(uint256(mockId) + 1); + + // Mock request data + bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); + + // Store the mock request + // bondedDisputeModule.forTest_setRequestData(mockId, _requestData); + + // ------------------------------------ + // Scenario: dispute won by proposer + // ------------------------------------ + + mockDispute = + IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); + + // Mock and expect the call to pay, from¨*proposer to disputer* + _mockAndExpect( + address(accountingExtension), + abi.encodeCall(accountingExtension.pay, (_requestId, _proposer, _disputer, _token, _bondSize)), + abi.encode() + ); + + // Mock and expect the call to release, to the disputer + _mockAndExpect( + address(accountingExtension), + abi.encodeCall(accountingExtension.release, (_disputer, _requestId, _token, _bondSize)), + abi.encode() + ); + + vm.prank(address(oracle)); + bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); + + // ------------------------------------ + // Scenario: dispute loss by proposer + // ------------------------------------ + + mockDispute = + IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); + + // Mock and expect the call to pay, from *disputer to proposer* + _mockAndExpect( + address(accountingExtension), + abi.encodeCall(accountingExtension.pay, (_requestId, _disputer, _proposer, _token, _bondSize)), + abi.encode() + ); + + // Mock and expect the call to release, for the proposer + _mockAndExpect( + address(accountingExtension), + abi.encodeCall(accountingExtension.release, (_proposer, _requestId, _token, _bondSize)), + abi.encode() + ); + + vm.prank(address(oracle)); + bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); + + // ------------------------------------ + // Scenario: dispute with no resolution + // ------------------------------------ + + mockDispute = + IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); + + // Mock and expect the call to release, for the proposer + _mockAndExpect( + address(accountingExtension), + abi.encodeCall(accountingExtension.release, (_proposer, _requestId, _token, _bondSize)), + abi.encode() + ); + + // Mock and expect the call to release, for the disputer + _mockAndExpect( + address(accountingExtension), + abi.encodeCall(accountingExtension.release, (_disputer, _requestId, _token, _bondSize)), + abi.encode() + ); + + vm.prank(address(oracle)); + bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); + } + + function test_statusWithNoChange(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { + // Mock id's (insure they are different) + bytes32 _requestId = mockId; + bytes32 _responseId = bytes32(uint256(mockId) + 1); + + // Mock request data + bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); + + // Store the mock request + // bondedDisputeModule.forTest_setRequestData(mockId, _requestData); + + // ------------------------------------ + // Scenario: dispute new status is None + // ------------------------------------ + + mockDispute = + IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); + + // Expect the event + vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); + emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.None); + + vm.prank(address(oracle)); + bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); + + // ------------------------------------ + // Scenario: dispute new status is Active + // ------------------------------------ + + mockDispute = + IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); + + // Expect the event + vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); + emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.Active); + + vm.prank(address(oracle)); + bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); + // ------------------------------------ + // Scenario: dispute new status is Escalated + // ------------------------------------ + + mockDispute = + IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); + + // Expect the event + vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); + emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.Escalated); + + vm.prank(address(oracle)); + bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); + } + + function test_emitsEvent(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { + // Mock id's (insure they are different) + bytes32 _requestId = mockId; + bytes32 _responseId = bytes32(uint256(mockId) + 1); + + // Mock request data + bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); + + // Store the mock request + // bondedDisputeModule.forTest_setRequestData(mockId, _requestData); + + // ------------------------------------ + // Scenario: dispute won by proposer + // ------------------------------------ + + mockDispute = + IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); + + // Mock and expect the call to pay, from¨*proposer to disputer* + _mockAndExpect( + address(accountingExtension), + abi.encodeCall(accountingExtension.pay, (_requestId, _proposer, _disputer, _token, _bondSize)), + abi.encode() + ); + + // Mock and expect the call to release, to the disputer + _mockAndExpect( + address(accountingExtension), + abi.encodeCall(accountingExtension.release, (_disputer, _requestId, _token, _bondSize)), + abi.encode() + ); + + // Expect the event + vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); + emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.Won); + + vm.prank(address(oracle)); + bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); + } + + /** + * @notice Test if onDisputeStatusChange reverts when called by caller who's not the oracle + */ + function test_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); + bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); + } +} + +contract BondedResponseModule_Unit_DisputeResponse is BaseTest { + /** + * @notice Test if dispute response returns the correct status + */ + function test_createBond(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { + // Mock id's (insure they are different) + bytes32 _requestId = mockId; + bytes32 _responseId = bytes32(uint256(mockId) + 1); + + // Mock request data + bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); + + // Store the mock request + // bondedDisputeModule.forTest_setRequestData(mockId, _requestData); + + // Mock and expect the call to the accounting extension, initiating the bond + _mockAndExpect( + address(accountingExtension), + abi.encodeWithSignature('bond(address,bytes32,address,uint256)', _disputer, _requestId, _token, _bondSize), + abi.encode() + ); + + // Test: call disputeResponse + vm.prank(address(oracle)); + bondedDisputeModule.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: revert if wrong caller + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + + // Test: call disputeResponse from non-oracle address + vm.prank(_randomCaller); + bondedDisputeModule.disputeResponse(mockRequest, mockResponse, mockDispute); + } +} diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 46874489..5d7691cf 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -1,56 +1,54 @@ -// // 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 {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; -// import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; -// contract Helpers is DSTestPlus { -// modifier assumeFuzzable(address _address) { -// _assumeFuzzable(_address); -// _; -// } +contract Helpers is DSTestPlus { + modifier assumeFuzzable(address _address) { + _assumeFuzzable(_address); + _; + } -// function _assumeFuzzable(address _address) internal pure { -// assumeNotForgeAddress(_address); -// assumeNotZeroAddress(_address); -// assumeNotPrecompile(_address); -// } + function _assumeFuzzable(address _address) internal pure { + assumeNotForgeAddress(_address); + assumeNotZeroAddress(_address); + assumeNotPrecompile(_address); + } -// function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { -// vm.mockCall(_receiver, _calldata, _returned); -// vm.expectCall(_receiver, _calldata); -// } + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } -// function _getMockDispute( -// bytes32 _requestId, -// address _disputer, -// address _proposer -// ) internal view returns (IOracle.Dispute memory _dispute) { -// _dispute = IOracle.Dispute({ -// disputer: _disputer, -// responseId: bytes32('response'), -// proposer: _proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.None, -// createdAt: block.timestamp -// }); -// } + function _getMockDispute( + bytes32 _requestId, + address _disputer, + address _proposer + ) internal view returns (IOracle.Dispute memory _dispute) { + _dispute = IOracle.Dispute({ + disputer: _disputer, + responseId: bytes32('response'), + proposer: _proposer, + requestId: _requestId + }); + } -// function _forBondDepositERC20( -// IAccountingExtension _accountingExtension, -// address _depositor, -// IERC20 _token, -// uint256 _depositAmount, -// uint256 _balanceIncrease -// ) internal { -// vm.assume(_balanceIncrease >= _depositAmount); -// deal(address(_token), _depositor, _balanceIncrease); -// vm.startPrank(_depositor); -// _token.approve(address(_accountingExtension), _depositAmount); -// _accountingExtension.deposit(_token, _depositAmount); -// vm.stopPrank(); -// } -// } + function _forBondDepositERC20( + IAccountingExtension _accountingExtension, + address _depositor, + IERC20 _token, + uint256 _depositAmount, + uint256 _balanceIncrease + ) internal { + vm.assume(_balanceIncrease >= _depositAmount); + deal(address(_token), _depositor, _balanceIncrease); + vm.startPrank(_depositor); + _token.approve(address(_accountingExtension), _depositAmount); + _accountingExtension.deposit(_token, _depositAmount); + vm.stopPrank(); + } +} From f9261e420cb28d0f142d179e21130959588ee3a5 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Mon, 6 Nov 2023 12:37:39 +0400 Subject: [PATCH 02/53] fix: helpers and accounting --- .../extensions/IAccountingExtension.sol | 380 +++++++++--------- solidity/test/utils/Helpers.sol | 96 +++-- 2 files changed, 237 insertions(+), 239 deletions(-) diff --git a/solidity/interfaces/extensions/IAccountingExtension.sol b/solidity/interfaces/extensions/IAccountingExtension.sol index 5a42243e..74c9c9cd 100644 --- a/solidity/interfaces/extensions/IAccountingExtension.sol +++ b/solidity/interfaces/extensions/IAccountingExtension.sol @@ -1,190 +1,190 @@ -// // 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'; - -// /* -// * @title AccountingExtension -// * @notice Extension allowing users to deposit and bond funds -// * to be used for payments and disputes. -// */ -// interface IAccountingExtension { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice A user deposited tokens into the accounting extension -// * @param _depositor The user who deposited the tokens -// * @param _token The address of the token deposited by the user -// * @param _amount The amount of `_token` deposited -// */ -// event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A user withdrew tokens from the accounting extension -// * @param _withdrawer The user who withdrew the tokens -// * @param _token The address of the token withdrawn by the user -// * @param _amount The amount of `_token` withdrawn -// */ -// event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A payment between users has been made -// * @param _beneficiary The user receiving the tokens -// * @param _payer The user who is getting its tokens transferred -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` transferred -// */ -// event Paid( -// bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount -// ); - -// /** -// * @notice User's funds have been bonded -// * @param _bonder The user who is getting its tokens bonded -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` bonded -// */ -// event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice User's funds have been released -// * @param _beneficiary The user who is getting its tokens released -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` released -// */ -// event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Thrown when the account doesn't have enough balance to bond/withdraw -// * or not enough bonded to release/pay -// */ -// error AccountingExtension_InsufficientFunds(); - -// /** -// * @notice Thrown when the module bonding user tokens hasn't been approved by the user. -// */ -// error AccountingExtension_InsufficientAllowance(); - -// /** -// * @notice Thrown when an `onlyAllowedModule` function is called by something -// * else than a module being used in the corresponding request -// */ -// error AccountingExtension_UnauthorizedModule(); - -// /** -// * @notice Thrown when an `onlyParticipant` function is called with an address -// * that is not part of the request. -// */ -// error AccountingExtension_UnauthorizedUser(); - -// /*/////////////////////////////////////////////////////////////// -// VARIABLES -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Returns the interface for the Oracle contract -// */ -// function ORACLE() external view returns (IOracle _oracle); - -// /** -// * @notice Returns the amount of a token a user has bonded -// * @param _user The address of the user with bonded tokens -// * @param _bondToken The token bonded -// * @param _requestId The id of the request the user bonded for -// * @return _amount The amount of `_bondToken` bonded -// */ -// function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); - -// /** -// * @notice Returns the amount of a token a user has deposited -// * @param _user The address of the user with deposited tokens -// * @param _token The token deposited -// * @return _amount The amount of `_token` deposited -// */ -// function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Transfers tokens from a user and updates his virtual balance -// * @dev The user must have approved the accounting extension to transfer the tokens. -// * @param _token The address of the token being deposited -// * @param _amount The amount of `_token` to deposit -// */ -// function deposit(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows an user to withdraw deposited tokens -// * @param _token The address of the token being withdrawn -// * @param _amount The amount of `_token` to withdraw -// */ -// function withdraw(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to transfer bonded tokens from one user to another -// * @dev Only the virtual balances in the accounting extension are modified. The token contract -// * is not called nor its balances modified. -// * @param _requestId The id of the request handling the user's tokens -// * @param _payer The address of the user paying the tokens -// * @param _receiver The address of the user receiving the tokens -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` being transferred -// */ -// function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a valid module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// * @param _sender The address starting the propose call on the Oracle -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; - -// /** -// * @notice Allows a valid module to release a user's tokens -// * @param _bonder The address of the user to release tokens for -// * @param _requestId The id of the request where the tokens were bonded -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` to release -// */ -// function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a user to approve a module for bonding tokens -// * @param _module The address of the module to be approved -// */ -// function approveModule(address _module) external; - -// /** -// * @notice Allows a user to revoke a module's approval for bonding tokens -// * @param _module The address of the module to be revoked -// */ -// function revokeModule(address _module) external; - -// /** -// * @notice Returns a list of all modules a user has approved -// * @param _user The address of the user -// * @return _approvedModules The array of all modules approved by the user -// */ -// function approvedModules(address _user) external view returns (address[] memory _approvedModules); -// } +// 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'; + +/* + * @title AccountingExtension + * @notice Extension allowing users to deposit and bond funds + * to be used for payments and disputes. + */ +interface IAccountingExtension { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A user deposited tokens into the accounting extension + * @param _depositor The user who deposited the tokens + * @param _token The address of the token deposited by the user + * @param _amount The amount of `_token` deposited + */ + event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A user withdrew tokens from the accounting extension + * @param _withdrawer The user who withdrew the tokens + * @param _token The address of the token withdrawn by the user + * @param _amount The amount of `_token` withdrawn + */ + event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A payment between users has been made + * @param _beneficiary The user receiving the tokens + * @param _payer The user who is getting its tokens transferred + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` transferred + */ + event Paid( + bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount + ); + + /** + * @notice User's funds have been bonded + * @param _bonder The user who is getting its tokens bonded + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` bonded + */ + event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); + + /** + * @notice User's funds have been released + * @param _beneficiary The user who is getting its tokens released + * @param _token The address of the token being released + * @param _amount The amount of `_token` released + */ + event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Thrown when the account doesn't have enough balance to bond/withdraw + * or not enough bonded to release/pay + */ + error AccountingExtension_InsufficientFunds(); + + /** + * @notice Thrown when the module bonding user tokens hasn't been approved by the user. + */ + error AccountingExtension_InsufficientAllowance(); + + /** + * @notice Thrown when an `onlyAllowedModule` function is called by something + * else than a module being used in the corresponding request + */ + error AccountingExtension_UnauthorizedModule(); + + /** + * @notice Thrown when an `onlyParticipant` function is called with an address + * that is not part of the request. + */ + error AccountingExtension_UnauthorizedUser(); + + /*/////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the interface for the Oracle contract + */ + function ORACLE() external view returns (IOracle _oracle); + + /** + * @notice Returns the amount of a token a user has bonded + * @param _user The address of the user with bonded tokens + * @param _bondToken The token bonded + * @param _requestId The id of the request the user bonded for + * @return _amount The amount of `_bondToken` bonded + */ + function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); + + /** + * @notice Returns the amount of a token a user has deposited + * @param _user The address of the user with deposited tokens + * @param _token The token deposited + * @return _amount The amount of `_token` deposited + */ + function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Transfers tokens from a user and updates his virtual balance + * @dev The user must have approved the accounting extension to transfer the tokens. + * @param _token The address of the token being deposited + * @param _amount The amount of `_token` to deposit + */ + function deposit(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows an user to withdraw deposited tokens + * @param _token The address of the token being withdrawn + * @param _amount The amount of `_token` to withdraw + */ + function withdraw(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to transfer bonded tokens from one user to another + * @dev Only the virtual balances in the accounting extension are modified. The token contract + * is not called nor its balances modified. + * @param _requestId The id of the request handling the user's tokens + * @param _payer The address of the user paying the tokens + * @param _receiver The address of the user receiving the tokens + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` being transferred + */ + function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a valid module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + * @param _sender The address starting the propose call on the Oracle + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; + + /** + * @notice Allows a valid module to release a user's tokens + * @param _bonder The address of the user to release tokens for + * @param _requestId The id of the request where the tokens were bonded + * @param _token The address of the token being released + * @param _amount The amount of `_token` to release + */ + function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a user to approve a module for bonding tokens + * @param _module The address of the module to be approved + */ + function approveModule(address _module) external; + + /** + * @notice Allows a user to revoke a module's approval for bonding tokens + * @param _module The address of the module to be revoked + */ + function revokeModule(address _module) external; + + /** + * @notice Returns a list of all modules a user has approved + * @param _user The address of the user + * @return _approvedModules The array of all modules approved by the user + */ + function approvedModules(address _user) external view returns (address[] memory _approvedModules); +} diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 46874489..5d7691cf 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -1,56 +1,54 @@ -// // 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 {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; -// import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; -// contract Helpers is DSTestPlus { -// modifier assumeFuzzable(address _address) { -// _assumeFuzzable(_address); -// _; -// } +contract Helpers is DSTestPlus { + modifier assumeFuzzable(address _address) { + _assumeFuzzable(_address); + _; + } -// function _assumeFuzzable(address _address) internal pure { -// assumeNotForgeAddress(_address); -// assumeNotZeroAddress(_address); -// assumeNotPrecompile(_address); -// } + function _assumeFuzzable(address _address) internal pure { + assumeNotForgeAddress(_address); + assumeNotZeroAddress(_address); + assumeNotPrecompile(_address); + } -// function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { -// vm.mockCall(_receiver, _calldata, _returned); -// vm.expectCall(_receiver, _calldata); -// } + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } -// function _getMockDispute( -// bytes32 _requestId, -// address _disputer, -// address _proposer -// ) internal view returns (IOracle.Dispute memory _dispute) { -// _dispute = IOracle.Dispute({ -// disputer: _disputer, -// responseId: bytes32('response'), -// proposer: _proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.None, -// createdAt: block.timestamp -// }); -// } + function _getMockDispute( + bytes32 _requestId, + address _disputer, + address _proposer + ) internal view returns (IOracle.Dispute memory _dispute) { + _dispute = IOracle.Dispute({ + disputer: _disputer, + responseId: bytes32('response'), + proposer: _proposer, + requestId: _requestId + }); + } -// function _forBondDepositERC20( -// IAccountingExtension _accountingExtension, -// address _depositor, -// IERC20 _token, -// uint256 _depositAmount, -// uint256 _balanceIncrease -// ) internal { -// vm.assume(_balanceIncrease >= _depositAmount); -// deal(address(_token), _depositor, _balanceIncrease); -// vm.startPrank(_depositor); -// _token.approve(address(_accountingExtension), _depositAmount); -// _accountingExtension.deposit(_token, _depositAmount); -// vm.stopPrank(); -// } -// } + function _forBondDepositERC20( + IAccountingExtension _accountingExtension, + address _depositor, + IERC20 _token, + uint256 _depositAmount, + uint256 _balanceIncrease + ) internal { + vm.assume(_balanceIncrease >= _depositAmount); + deal(address(_token), _depositor, _balanceIncrease); + vm.startPrank(_depositor); + _token.approve(address(_accountingExtension), _depositAmount); + _accountingExtension.deposit(_token, _depositAmount); + vm.stopPrank(); + } +} From c1e254b1969af64735c166e8337f370cc3f34944 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Mon, 6 Nov 2023 20:19:17 +0400 Subject: [PATCH 03/53] perf: optimize `SparseMerkleTreeRequestModule` --- .../sparse_merkle_tree_request_module.md | 2 +- .../request/SparseMerkleTreeRequestModule.sol | 112 ++-- .../ISparseMerkleTreeRequestModule.sol | 123 ++-- .../SparseMerkleTreeRequestModule.t.sol | 626 ++++++++---------- 4 files changed, 388 insertions(+), 475 deletions(-) diff --git a/docs/src/content/modules/request/sparse_merkle_tree_request_module.md b/docs/src/content/modules/request/sparse_merkle_tree_request_module.md index aa71ffe6..44fc3060 100644 --- a/docs/src/content/modules/request/sparse_merkle_tree_request_module.md +++ b/docs/src/content/modules/request/sparse_merkle_tree_request_module.md @@ -10,7 +10,7 @@ The `SparseMerkleTreeRequestModule` is a contract that allows a user to request ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: This function decodes the request data for a given request ID. It returns a RequestParameters struct that contains the parameters for the request. +- `decodeRequestData(bytes calldata _data)`: This function decodes the request data for a given request ID. It returns a RequestParameters struct that contains the parameters for the request. - `finalizeRequest(bytes32 _requestId, address _finalizer)`: This function is called by the Oracle to finalize the request. It either pays the proposer for the response or releases the requester's bond if no response was submitted. ### Request Parameters diff --git a/solidity/contracts/modules/request/SparseMerkleTreeRequestModule.sol b/solidity/contracts/modules/request/SparseMerkleTreeRequestModule.sol index f72f07a1..899f2958 100644 --- a/solidity/contracts/modules/request/SparseMerkleTreeRequestModule.sol +++ b/solidity/contracts/modules/request/SparseMerkleTreeRequestModule.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 {ISparseMerkleTreeRequestModule} from '../../../interfaces/modules/request/ISparseMerkleTreeRequestModule.sol'; +import {ISparseMerkleTreeRequestModule} from '../../../interfaces/modules/request/ISparseMerkleTreeRequestModule.sol'; -// contract SparseMerkleTreeRequestModule is Module, ISparseMerkleTreeRequestModule { -// constructor(IOracle _oracle) Module(_oracle) {} +contract SparseMerkleTreeRequestModule is Module, ISparseMerkleTreeRequestModule { + constructor(IOracle _oracle) Module(_oracle) {} -// /// @inheritdoc IModule -// function moduleName() public pure returns (string memory _moduleName) { -// _moduleName = 'SparseMerkleTreeRequestModule'; -// } + /// @inheritdoc IModule + function moduleName() public pure returns (string memory _moduleName) { + _moduleName = 'SparseMerkleTreeRequestModule'; + } -// /// @inheritdoc ISparseMerkleTreeRequestModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } + /// @inheritdoc ISparseMerkleTreeRequestModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } -// /** -// * @notice Hook triggered after setting up a request. Bonds the requester's payment amount -// * @param _requestId The ID of the request being setup -// */ -// 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 ISparseMerkleTreeRequestModule + function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external { + RequestParameters memory _params = decodeRequestData(_data); -// /// @inheritdoc ISparseMerkleTreeRequestModule -// function finalizeRequest( -// bytes32 _requestId, -// address _finalizer -// ) external override(ISparseMerkleTreeRequestModule, 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 ISparseMerkleTreeRequestModule + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external override(ISparseMerkleTreeRequestModule, 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/ISparseMerkleTreeRequestModule.sol b/solidity/interfaces/modules/request/ISparseMerkleTreeRequestModule.sol index 67590e91..8357ee8e 100644 --- a/solidity/interfaces/modules/request/ISparseMerkleTreeRequestModule.sol +++ b/solidity/interfaces/modules/request/ISparseMerkleTreeRequestModule.sol @@ -1,57 +1,66 @@ -// // 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 {ITreeVerifier} from '../../../interfaces/ITreeVerifier.sol'; -// import {IAccountingExtension} from '../../../interfaces/extensions/IAccountingExtension.sol'; - -// /* -// * @title SparseMerkleTreeRequestModule -// * @notice Module allowing a user to request the calculation -// * of a Merkle tree root from a set of leaves. -// */ -// interface ISparseMerkleTreeRequestModule is IRequestModule { -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Parameters of the request as stored in the module -// * @param treeData The encoded Merkle tree data parameters for the tree verifier -// * @param leavesToInsert The array of leaves to insert into the Merkle tree -// * @param treeVerifier The tree verifier to calculate the root -// * @param accountingExtension The accounting extension to use for the request -// * @param paymentToken The payment token to use for the request -// * @param paymentAmount The payment amount to use for the request -// */ -// struct RequestParameters { -// bytes treeData; -// bytes32[] leavesToInsert; -// ITreeVerifier treeVerifier; -// IAccountingExtension accountingExtension; -// IERC20 paymentToken; -// uint256 paymentAmount; -// } - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @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 Called by the Oracle to finalize the request by paying the proposer for the response -// * or releasing the requester's bond if no response was submitted -// * @param _requestId The ID of the request being finalized -// * @param _finalizer The address of the user who triggered the finalization of the request -// */ -// function finalizeRequest(bytes32 _requestId, address _finalizer) external; -// } +// 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 {IRequestModule} from + '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/request/IRequestModule.sol'; + +import {ITreeVerifier} from '../../../interfaces/ITreeVerifier.sol'; +import {IAccountingExtension} from '../../../interfaces/extensions/IAccountingExtension.sol'; + +/* + * @title SparseMerkleTreeRequestModule + * @notice Module allowing a user to request the calculation + * of a Merkle tree root from a set of leaves. + */ +interface ISparseMerkleTreeRequestModule is IRequestModule { + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Parameters of the request as stored in the module + * @param treeData The encoded Merkle tree data parameters for the tree verifier + * @param leavesToInsert The array of leaves to insert into the Merkle tree + * @param treeVerifier The tree verifier to calculate the root + * @param accountingExtension The accounting extension to use for the request + * @param paymentToken The payment token to use for the request + * @param paymentAmount The payment amount to use for the request + */ + struct RequestParameters { + bytes treeData; + bytes32[] leavesToInsert; + ITreeVerifier treeVerifier; + IAccountingExtension accountingExtension; + IERC20 paymentToken; + uint256 paymentAmount; + } + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @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); + + /// @inheritdoc IRequestModule + 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/SparseMerkleTreeRequestModule.t.sol b/solidity/test/unit/modules/request/SparseMerkleTreeRequestModule.t.sol index 7623cc70..051a1e3d 100644 --- a/solidity/test/unit/modules/request/SparseMerkleTreeRequestModule.t.sol +++ b/solidity/test/unit/modules/request/SparseMerkleTreeRequestModule.t.sol @@ -1,360 +1,266 @@ -// // 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 { -// SparseMerkleTreeRequestModule, -// ISparseMerkleTreeRequestModule -// } from '../../../../contracts/modules/request/SparseMerkleTreeRequestModule.sol'; - -// import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; -// import {ITreeVerifier} from '../../../../interfaces/ITreeVerifier.sol'; - -// /** -// * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks -// */ - -// contract ForTest_SparseMerkleTreeRequestModule is SparseMerkleTreeRequestModule { -// constructor(IOracle _oracle) SparseMerkleTreeRequestModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } -// } - -// /** -// * @title Sparse Merkle Tree Request Module Unit tests -// */ -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_SparseMerkleTreeRequestModule public sparseMerkleTreeRequestModule; -// // A mock oracle -// IOracle public oracle; -// // A mock accounting extension -// IAccountingExtension public accounting; -// // A mock tree verifier -// ITreeVerifier public treeVerifier; -// // Mock data for the request -// bytes32[32] internal _treeBranches = [ -// bytes32('branch1'), -// bytes32('branch2'), -// bytes32('branch3'), -// bytes32('branch4'), -// bytes32('branch5'), -// bytes32('branch6'), -// bytes32('branch7'), -// bytes32('branch8'), -// bytes32('branch9'), -// bytes32('branch10'), -// bytes32('branch11'), -// bytes32('branch12'), -// bytes32('branch13'), -// bytes32('branch14'), -// bytes32('branch15'), -// bytes32('branch16'), -// bytes32('branch17'), -// bytes32('branch18'), -// bytes32('branch19'), -// bytes32('branch20'), -// bytes32('branch21'), -// bytes32('branch22'), -// bytes32('branch23'), -// bytes32('branch24'), -// bytes32('branch25'), -// bytes32('branch26'), -// bytes32('branch27'), -// bytes32('branch28'), -// bytes32('branch29'), -// bytes32('branch30'), -// bytes32('branch31'), -// bytes32('branch32') -// ]; -// uint256 internal _treeCount = 1; -// bytes internal _treeData = abi.encode(_treeBranches, _treeCount); -// bytes32[] internal _leavesToInsert = [bytes32('leave1'), bytes32('leave2')]; - -// 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'); -// treeVerifier = ITreeVerifier(makeAddr('TreeVerifier')); -// vm.etch(address(accounting), hex'069420'); - -// sparseMerkleTreeRequestModule = new ForTest_SparseMerkleTreeRequestModule(oracle); -// } -// } - -// contract SparseMerkleTreeRequestModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(sparseMerkleTreeRequestModule.moduleName(), 'SparseMerkleTreeRequestModule', 'Wrong module name'); -// } - -// /** -// * @notice Test that the decodeRequestData function returns the correct values -// */ -// function test_decodeRequestData( -// bytes32 _requestId, -// IERC20 _paymentToken, -// uint256 _paymentAmount, -// IAccountingExtension _accounting, -// ITreeVerifier _treeVerifier -// ) public { -// bytes memory _requestData = abi.encode( -// ISparseMerkleTreeRequestModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: _treeVerifier, -// accountingExtension: _accounting, -// paymentToken: _paymentToken, -// paymentAmount: _paymentAmount -// }) -// ); - -// // Set the request data -// sparseMerkleTreeRequestModule.forTest_setRequestData(_requestId, _requestData); - -// // Decode the given request data -// ISparseMerkleTreeRequestModule.RequestParameters memory _params = -// sparseMerkleTreeRequestModule.decodeRequestData(_requestId); - -// (bytes32[32] memory _decodedTreeBranches, uint256 _decodedTreeCount) = -// abi.decode(_params.treeData, (bytes32[32], uint256)); - -// // Check: decoded values match original values? -// for (uint256 _i = 0; _i < _treeBranches.length; _i++) { -// assertEq(_decodedTreeBranches[_i], _treeBranches[_i], 'Mismatch: decoded tree branch'); -// } -// for (uint256 _i = 0; _i < _leavesToInsert.length; _i++) { -// assertEq(_params.leavesToInsert[_i], _leavesToInsert[_i], 'Mismatch: decoded leave to insert'); -// } -// assertEq(_decodedTreeCount, _treeCount, 'Mismatch: decoded tree count'); -// assertEq(address(_params.treeVerifier), address(_treeVerifier), 'Mismatch: decoded tree verifier'); -// 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 SparseMerkleTreeRequestModule_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, -// IAccountingExtension _accounting, -// ITreeVerifier _treeVerifier -// ) public assumeFuzzable(address(_accounting)) { -// bytes memory _requestData = abi.encode( -// ISparseMerkleTreeRequestModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: _treeVerifier, -// 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)); -// sparseMerkleTreeRequestModule.setupRequest(_requestId, _requestData); - -// // Check: request data was set? -// assertEq(sparseMerkleTreeRequestModule.requestData(_requestId), _requestData, 'Mismatch: Request data'); -// } -// } - -// contract SparseMerkleTreeRequestModule_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, -// IAccountingExtension _accounting, -// ITreeVerifier _treeVerifier -// ) public assumeFuzzable(address(_accounting)) { -// // Use the correct accounting parameters -// bytes memory _requestData = abi.encode( -// ISparseMerkleTreeRequestModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: _treeVerifier, -// 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 -// sparseMerkleTreeRequestModule.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 IOracle.getFinalizedResponse to be called -// vm.mockCall(address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse)); - -// // Mock and expect IAccountingExtension.pay to be called -// vm.mockCall( -// address(_accounting), -// abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _paymentToken, _paymentAmount)), -// abi.encode() -// ); - -// vm.startPrank(address(oracle)); -// sparseMerkleTreeRequestModule.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) -// ); - -// sparseMerkleTreeRequestModule.finalizeRequest(_requestId, address(this)); -// } - -// function test_emitsEvent( -// bytes32 _requestId, -// address _requester, -// address _proposer, -// IERC20 _paymentToken, -// uint256 _paymentAmount, -// IAccountingExtension _accounting, -// ITreeVerifier _treeVerifier -// ) public assumeFuzzable(address(_accounting)) { -// // Use the correct accounting parameters -// bytes memory _requestData = abi.encode( -// ISparseMerkleTreeRequestModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: _treeVerifier, -// 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 -// sparseMerkleTreeRequestModule.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 IOracle.getFinalizedResponse to be called -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse) -// ); - -// // Mock and expect IOracle.getRequest to be called -// _mockAndExpect( -// address(_accounting), -// abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _paymentToken, _paymentAmount)), -// abi.encode() -// ); - -// vm.startPrank(address(oracle)); -// sparseMerkleTreeRequestModule.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 IOracle.getRequest 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(sparseMerkleTreeRequestModule)); -// emit RequestFinalized(_requestId, address(this)); - -// sparseMerkleTreeRequestModule.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); -// sparseMerkleTreeRequestModule.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 { + SparseMerkleTreeRequestModule, + ISparseMerkleTreeRequestModule +} from '../../../../contracts/modules/request/SparseMerkleTreeRequestModule.sol'; + +import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; +import {ITreeVerifier} from '../../../../interfaces/ITreeVerifier.sol'; + +/** + * @title Sparse Merkle Tree Request Module Unit tests + */ +contract BaseTest is Test, Helpers { + // The target contract + SparseMerkleTreeRequestModule public sparseMerkleTreeRequestModule; + // A mock oracle + IOracle public oracle; + // A mock accounting extension + IAccountingExtension public accounting; + // A mock tree verifier + ITreeVerifier public treeVerifier; + // Create a new dummy response + IOracle.Response public mockResponse; + // Create a new dummy dispute + IOracle.Dispute public mockDispute; + // 100% random sequence of bytes representing request, response, or dispute id + bytes32 public mockId = bytes32('69'); + address internal _disputer = makeAddr('disputer'); + address internal _proposer = makeAddr('proposer'); + + // Mock data for the request + bytes32[32] internal _treeBranches = [ + bytes32('branch1'), + bytes32('branch2'), + bytes32('branch3'), + bytes32('branch4'), + bytes32('branch5'), + bytes32('branch6'), + bytes32('branch7'), + bytes32('branch8'), + bytes32('branch9'), + bytes32('branch10'), + bytes32('branch11'), + bytes32('branch12'), + bytes32('branch13'), + bytes32('branch14'), + bytes32('branch15'), + bytes32('branch16'), + bytes32('branch17'), + bytes32('branch18'), + bytes32('branch19'), + bytes32('branch20'), + bytes32('branch21'), + bytes32('branch22'), + bytes32('branch23'), + bytes32('branch24'), + bytes32('branch25'), + bytes32('branch26'), + bytes32('branch27'), + bytes32('branch28'), + bytes32('branch29'), + bytes32('branch30'), + bytes32('branch31'), + bytes32('branch32') + ]; + uint256 internal _treeCount = 1; + bytes internal _treeData = abi.encode(_treeBranches, _treeCount); + bytes32[] internal _leavesToInsert = [bytes32('leave1'), bytes32('leave2')]; + + 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'); + treeVerifier = ITreeVerifier(makeAddr('TreeVerifier')); + vm.etch(address(accounting), hex'069420'); + + sparseMerkleTreeRequestModule = new SparseMerkleTreeRequestModule(oracle); + + mockDispute = IOracle.Dispute({disputer: _disputer, responseId: mockId, proposer: _proposer, requestId: mockId}); + mockResponse = IOracle.Response({proposer: _proposer, requestId: mockId, response: bytes('')}); + } +} + +contract SparseMerkleTreeRequestModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(sparseMerkleTreeRequestModule.moduleName(), 'SparseMerkleTreeRequestModule', 'Wrong module name'); + } + + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData( + bytes32 _requestId, + IERC20 _paymentToken, + uint256 _paymentAmount, + IAccountingExtension _accounting, + ITreeVerifier _treeVerifier + ) public { + bytes memory _requestData = abi.encode( + ISparseMerkleTreeRequestModule.RequestParameters({ + treeData: _treeData, + leavesToInsert: _leavesToInsert, + treeVerifier: _treeVerifier, + accountingExtension: _accounting, + paymentToken: _paymentToken, + paymentAmount: _paymentAmount + }) + ); + + // Decode the given request data + ISparseMerkleTreeRequestModule.RequestParameters memory _params = + sparseMerkleTreeRequestModule.decodeRequestData(_requestData); + + (bytes32[32] memory _decodedTreeBranches, uint256 _decodedTreeCount) = + abi.decode(_params.treeData, (bytes32[32], uint256)); + + // Check: decoded values match original values? + for (uint256 _i = 0; _i < _treeBranches.length; _i++) { + assertEq(_decodedTreeBranches[_i], _treeBranches[_i], 'Mismatch: decoded tree branch'); + } + for (uint256 _i = 0; _i < _leavesToInsert.length; _i++) { + assertEq(_params.leavesToInsert[_i], _leavesToInsert[_i], 'Mismatch: decoded leave to insert'); + } + assertEq(_decodedTreeCount, _treeCount, 'Mismatch: decoded tree count'); + assertEq(address(_params.treeVerifier), address(_treeVerifier), 'Mismatch: decoded tree verifier'); + 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 SparseMerkleTreeRequestModule_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, + IAccountingExtension _accounting, + ITreeVerifier _treeVerifier, + IOracle.Request calldata _request + ) public assumeFuzzable(address(_accounting)) { + // Use the correct accounting parameters + bytes memory _requestData = abi.encode( + ISparseMerkleTreeRequestModule.RequestParameters({ + treeData: _treeData, + leavesToInsert: _leavesToInsert, + treeVerifier: _treeVerifier, + accountingExtension: _accounting, + paymentToken: _paymentToken, + paymentAmount: _paymentAmount + }) + ); + + IOracle.Request memory _fullRequest; + _fullRequest.requester = _requester; + + IOracle.Response memory _fullResponse; + _fullResponse.proposer = _proposer; + + // Mock and expect IAccountingExtension.pay to be called + vm.mockCall( + address(_accounting), + abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _paymentToken, _paymentAmount)), + abi.encode() + ); + + vm.startPrank(address(oracle)); + sparseMerkleTreeRequestModule.finalizeRequest(_request, mockResponse, address(oracle)); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(_accounting), + abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _paymentToken, _paymentAmount)), + abi.encode(true) + ); + + sparseMerkleTreeRequestModule.finalizeRequest(_request, mockResponse, address(this)); + } + + function test_emitsEvent( + bytes32 _requestId, + address _requester, + address _proposer, + IERC20 _paymentToken, + uint256 _paymentAmount, + IAccountingExtension _accounting, + ITreeVerifier _treeVerifier, + IOracle.Request calldata _request + ) public assumeFuzzable(address(_accounting)) { + // Use the correct accounting parameters + bytes memory _requestData = abi.encode( + ISparseMerkleTreeRequestModule.RequestParameters({ + treeData: _treeData, + leavesToInsert: _leavesToInsert, + treeVerifier: _treeVerifier, + accountingExtension: _accounting, + paymentToken: _paymentToken, + paymentAmount: _paymentAmount + }) + ); + + // Mock and expect pay to be called + _mockAndExpect( + address(_accounting), + abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _paymentToken, _paymentAmount)), + abi.encode() + ); + + vm.startPrank(address(oracle)); + sparseMerkleTreeRequestModule.finalizeRequest(_request, mockResponse, address(oracle)); + + // Mock and expect 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(sparseMerkleTreeRequestModule)); + emit RequestFinalized(_requestId, address(this)); + + sparseMerkleTreeRequestModule.finalizeRequest(_request, mockResponse, address(this)); + } + + /** + * @notice Test that the finalizeRequest reverts if caller is not the oracle + */ + function test_revertsIfWrongCaller(IOracle.Request calldata _request, 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); + sparseMerkleTreeRequestModule.finalizeRequest(_request, mockResponse, address(_caller)); + } +} From 599d96f685dd7234d54766f6db86e25332dec9e0 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Mon, 6 Nov 2023 20:19:41 +0400 Subject: [PATCH 04/53] feat: uncomment helpers --- .../extensions/IAccountingExtension.sol | 380 +++++++++--------- solidity/test/utils/Helpers.sol | 122 +++--- 2 files changed, 256 insertions(+), 246 deletions(-) diff --git a/solidity/interfaces/extensions/IAccountingExtension.sol b/solidity/interfaces/extensions/IAccountingExtension.sol index 5a42243e..74c9c9cd 100644 --- a/solidity/interfaces/extensions/IAccountingExtension.sol +++ b/solidity/interfaces/extensions/IAccountingExtension.sol @@ -1,190 +1,190 @@ -// // 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'; - -// /* -// * @title AccountingExtension -// * @notice Extension allowing users to deposit and bond funds -// * to be used for payments and disputes. -// */ -// interface IAccountingExtension { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice A user deposited tokens into the accounting extension -// * @param _depositor The user who deposited the tokens -// * @param _token The address of the token deposited by the user -// * @param _amount The amount of `_token` deposited -// */ -// event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A user withdrew tokens from the accounting extension -// * @param _withdrawer The user who withdrew the tokens -// * @param _token The address of the token withdrawn by the user -// * @param _amount The amount of `_token` withdrawn -// */ -// event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A payment between users has been made -// * @param _beneficiary The user receiving the tokens -// * @param _payer The user who is getting its tokens transferred -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` transferred -// */ -// event Paid( -// bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount -// ); - -// /** -// * @notice User's funds have been bonded -// * @param _bonder The user who is getting its tokens bonded -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` bonded -// */ -// event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice User's funds have been released -// * @param _beneficiary The user who is getting its tokens released -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` released -// */ -// event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Thrown when the account doesn't have enough balance to bond/withdraw -// * or not enough bonded to release/pay -// */ -// error AccountingExtension_InsufficientFunds(); - -// /** -// * @notice Thrown when the module bonding user tokens hasn't been approved by the user. -// */ -// error AccountingExtension_InsufficientAllowance(); - -// /** -// * @notice Thrown when an `onlyAllowedModule` function is called by something -// * else than a module being used in the corresponding request -// */ -// error AccountingExtension_UnauthorizedModule(); - -// /** -// * @notice Thrown when an `onlyParticipant` function is called with an address -// * that is not part of the request. -// */ -// error AccountingExtension_UnauthorizedUser(); - -// /*/////////////////////////////////////////////////////////////// -// VARIABLES -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Returns the interface for the Oracle contract -// */ -// function ORACLE() external view returns (IOracle _oracle); - -// /** -// * @notice Returns the amount of a token a user has bonded -// * @param _user The address of the user with bonded tokens -// * @param _bondToken The token bonded -// * @param _requestId The id of the request the user bonded for -// * @return _amount The amount of `_bondToken` bonded -// */ -// function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); - -// /** -// * @notice Returns the amount of a token a user has deposited -// * @param _user The address of the user with deposited tokens -// * @param _token The token deposited -// * @return _amount The amount of `_token` deposited -// */ -// function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Transfers tokens from a user and updates his virtual balance -// * @dev The user must have approved the accounting extension to transfer the tokens. -// * @param _token The address of the token being deposited -// * @param _amount The amount of `_token` to deposit -// */ -// function deposit(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows an user to withdraw deposited tokens -// * @param _token The address of the token being withdrawn -// * @param _amount The amount of `_token` to withdraw -// */ -// function withdraw(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to transfer bonded tokens from one user to another -// * @dev Only the virtual balances in the accounting extension are modified. The token contract -// * is not called nor its balances modified. -// * @param _requestId The id of the request handling the user's tokens -// * @param _payer The address of the user paying the tokens -// * @param _receiver The address of the user receiving the tokens -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` being transferred -// */ -// function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a valid module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// * @param _sender The address starting the propose call on the Oracle -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; - -// /** -// * @notice Allows a valid module to release a user's tokens -// * @param _bonder The address of the user to release tokens for -// * @param _requestId The id of the request where the tokens were bonded -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` to release -// */ -// function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a user to approve a module for bonding tokens -// * @param _module The address of the module to be approved -// */ -// function approveModule(address _module) external; - -// /** -// * @notice Allows a user to revoke a module's approval for bonding tokens -// * @param _module The address of the module to be revoked -// */ -// function revokeModule(address _module) external; - -// /** -// * @notice Returns a list of all modules a user has approved -// * @param _user The address of the user -// * @return _approvedModules The array of all modules approved by the user -// */ -// function approvedModules(address _user) external view returns (address[] memory _approvedModules); -// } +// 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'; + +/* + * @title AccountingExtension + * @notice Extension allowing users to deposit and bond funds + * to be used for payments and disputes. + */ +interface IAccountingExtension { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A user deposited tokens into the accounting extension + * @param _depositor The user who deposited the tokens + * @param _token The address of the token deposited by the user + * @param _amount The amount of `_token` deposited + */ + event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A user withdrew tokens from the accounting extension + * @param _withdrawer The user who withdrew the tokens + * @param _token The address of the token withdrawn by the user + * @param _amount The amount of `_token` withdrawn + */ + event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A payment between users has been made + * @param _beneficiary The user receiving the tokens + * @param _payer The user who is getting its tokens transferred + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` transferred + */ + event Paid( + bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount + ); + + /** + * @notice User's funds have been bonded + * @param _bonder The user who is getting its tokens bonded + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` bonded + */ + event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); + + /** + * @notice User's funds have been released + * @param _beneficiary The user who is getting its tokens released + * @param _token The address of the token being released + * @param _amount The amount of `_token` released + */ + event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Thrown when the account doesn't have enough balance to bond/withdraw + * or not enough bonded to release/pay + */ + error AccountingExtension_InsufficientFunds(); + + /** + * @notice Thrown when the module bonding user tokens hasn't been approved by the user. + */ + error AccountingExtension_InsufficientAllowance(); + + /** + * @notice Thrown when an `onlyAllowedModule` function is called by something + * else than a module being used in the corresponding request + */ + error AccountingExtension_UnauthorizedModule(); + + /** + * @notice Thrown when an `onlyParticipant` function is called with an address + * that is not part of the request. + */ + error AccountingExtension_UnauthorizedUser(); + + /*/////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the interface for the Oracle contract + */ + function ORACLE() external view returns (IOracle _oracle); + + /** + * @notice Returns the amount of a token a user has bonded + * @param _user The address of the user with bonded tokens + * @param _bondToken The token bonded + * @param _requestId The id of the request the user bonded for + * @return _amount The amount of `_bondToken` bonded + */ + function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); + + /** + * @notice Returns the amount of a token a user has deposited + * @param _user The address of the user with deposited tokens + * @param _token The token deposited + * @return _amount The amount of `_token` deposited + */ + function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Transfers tokens from a user and updates his virtual balance + * @dev The user must have approved the accounting extension to transfer the tokens. + * @param _token The address of the token being deposited + * @param _amount The amount of `_token` to deposit + */ + function deposit(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows an user to withdraw deposited tokens + * @param _token The address of the token being withdrawn + * @param _amount The amount of `_token` to withdraw + */ + function withdraw(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to transfer bonded tokens from one user to another + * @dev Only the virtual balances in the accounting extension are modified. The token contract + * is not called nor its balances modified. + * @param _requestId The id of the request handling the user's tokens + * @param _payer The address of the user paying the tokens + * @param _receiver The address of the user receiving the tokens + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` being transferred + */ + function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a valid module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + * @param _sender The address starting the propose call on the Oracle + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; + + /** + * @notice Allows a valid module to release a user's tokens + * @param _bonder The address of the user to release tokens for + * @param _requestId The id of the request where the tokens were bonded + * @param _token The address of the token being released + * @param _amount The amount of `_token` to release + */ + function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a user to approve a module for bonding tokens + * @param _module The address of the module to be approved + */ + function approveModule(address _module) external; + + /** + * @notice Allows a user to revoke a module's approval for bonding tokens + * @param _module The address of the module to be revoked + */ + function revokeModule(address _module) external; + + /** + * @notice Returns a list of all modules a user has approved + * @param _user The address of the user + * @return _approvedModules The array of all modules approved by the user + */ + function approvedModules(address _user) external view returns (address[] memory _approvedModules); +} diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 46874489..af32e439 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -1,56 +1,66 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; - -// import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; - -// contract Helpers is DSTestPlus { -// modifier assumeFuzzable(address _address) { -// _assumeFuzzable(_address); -// _; -// } - -// function _assumeFuzzable(address _address) internal pure { -// assumeNotForgeAddress(_address); -// assumeNotZeroAddress(_address); -// assumeNotPrecompile(_address); -// } - -// function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { -// vm.mockCall(_receiver, _calldata, _returned); -// vm.expectCall(_receiver, _calldata); -// } - -// function _getMockDispute( -// bytes32 _requestId, -// address _disputer, -// address _proposer -// ) internal view returns (IOracle.Dispute memory _dispute) { -// _dispute = IOracle.Dispute({ -// disputer: _disputer, -// responseId: bytes32('response'), -// proposer: _proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.None, -// createdAt: block.timestamp -// }); -// } - -// function _forBondDepositERC20( -// IAccountingExtension _accountingExtension, -// address _depositor, -// IERC20 _token, -// uint256 _depositAmount, -// uint256 _balanceIncrease -// ) internal { -// vm.assume(_balanceIncrease >= _depositAmount); -// deal(address(_token), _depositor, _balanceIncrease); -// vm.startPrank(_depositor); -// _token.approve(address(_accountingExtension), _depositAmount); -// _accountingExtension.deposit(_token, _depositAmount); -// vm.stopPrank(); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; + +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; + +contract Helpers is DSTestPlus { + modifier assumeFuzzable(address _address) { + _assumeFuzzable(_address); + _; + } + + function _assumeFuzzable(address _address) internal pure { + assumeNotForgeAddress(_address); + assumeNotZeroAddress(_address); + assumeNotPrecompile(_address); + } + + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + function _getMockDispute( + bytes32 _requestId, + address _disputer, + address _proposer + ) internal view returns (IOracle.Dispute memory _dispute) { + _dispute = IOracle.Dispute({ + disputer: _disputer, + responseId: bytes32('response'), + proposer: _proposer, + requestId: _requestId + }); + } + + function _forBondDepositERC20( + IAccountingExtension _accountingExtension, + address _depositor, + IERC20 _token, + uint256 _depositAmount, + uint256 _balanceIncrease + ) internal { + vm.assume(_balanceIncrease >= _depositAmount); + deal(address(_token), _depositor, _balanceIncrease); + vm.startPrank(_depositor); + _token.approve(address(_accountingExtension), _depositAmount); + _accountingExtension.deposit(_token, _depositAmount); + vm.stopPrank(); + } + + function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_response)); + } + + function _getId(IOracle.Request memory _request) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_request)); + } + + function _getId(IOracle.Dispute memory _dispute) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_dispute)); + } +} From b9f72cb08db77a2b59df794e0af57709aecca218 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:20:54 +0400 Subject: [PATCH 05/53] perf: optimize `ContractCallRequestModule` --- .../request/contract_call_request_module.md | 4 +- .../request/ContractCallRequestModule.sol | 112 ++-- .../request/IContractCallRequestModule.sol | 91 ++-- .../request/ContractCallRequestModule.t.sol | 513 +++++++----------- 4 files changed, 311 insertions(+), 409 deletions(-) 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..84e3f616 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(bytes calldata _data)`: 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..578e552a 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 { + 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..c68dbff1 100644 --- a/solidity/interfaces/modules/request/IContractCallRequestModule.sol +++ b/solidity/interfaces/modules/request/IContractCallRequestModule.sol @@ -1,45 +1,56 @@ -// // 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; -// } + /// @inheritdoc IRequestModule + 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..5e15640d 100644 --- a/solidity/test/unit/modules/request/ContractCallRequestModule.t.sol +++ b/solidity/test/unit/modules/request/ContractCallRequestModule.t.sol @@ -1,310 +1,203 @@ -// // 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); + bytes32 public mockId = bytes32('69'); + address internal _proposer = makeAddr('proposer'); + // Create a new dummy response + IOracle.Response public mockResponse; + + 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 ContractCallRequestModule(oracle); + + mockResponse = IOracle.Response({proposer: _proposer, requestId: mockId, response: bytes('')}); + } +} + +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 + }) + ); + + // 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_makesCalls( + bytes32 _requestId, + address _requester, + address _proposer, + IERC20 _paymentToken, + uint256 _paymentAmount, + IOracle.Request calldata _request + ) 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 + }) + ); + + // 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(_request, mockResponse, address(oracle)); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _paymentToken, _paymentAmount)), + abi.encode(true) + ); + + contractCallRequestModule.finalizeRequest(_request, mockResponse, address(this)); + } + + function test_emitsEvent( + bytes32 _requestId, + address _requester, + address _proposer, + IERC20 _paymentToken, + uint256 _paymentAmount, + IOracle.Request calldata _request + ) 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 + }) + ); + + // 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(_request, mockResponse, address(oracle)); + + // 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(_request, mockResponse, address(this)); + } + + /** + * @notice Test that the finalizeRequest reverts if caller is not the oracle + */ + function test_revertsIfWrongCaller(bytes32 _requestId, 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)); + } +} From 196cda12366273bc002f272d4dd5569486312dac Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:21:55 +0400 Subject: [PATCH 06/53] feat: uncomment helpers --- .../extensions/IAccountingExtension.sol | 380 +++++++++--------- solidity/test/utils/Helpers.sol | 122 +++--- 2 files changed, 256 insertions(+), 246 deletions(-) diff --git a/solidity/interfaces/extensions/IAccountingExtension.sol b/solidity/interfaces/extensions/IAccountingExtension.sol index 5a42243e..74c9c9cd 100644 --- a/solidity/interfaces/extensions/IAccountingExtension.sol +++ b/solidity/interfaces/extensions/IAccountingExtension.sol @@ -1,190 +1,190 @@ -// // 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'; - -// /* -// * @title AccountingExtension -// * @notice Extension allowing users to deposit and bond funds -// * to be used for payments and disputes. -// */ -// interface IAccountingExtension { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice A user deposited tokens into the accounting extension -// * @param _depositor The user who deposited the tokens -// * @param _token The address of the token deposited by the user -// * @param _amount The amount of `_token` deposited -// */ -// event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A user withdrew tokens from the accounting extension -// * @param _withdrawer The user who withdrew the tokens -// * @param _token The address of the token withdrawn by the user -// * @param _amount The amount of `_token` withdrawn -// */ -// event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A payment between users has been made -// * @param _beneficiary The user receiving the tokens -// * @param _payer The user who is getting its tokens transferred -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` transferred -// */ -// event Paid( -// bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount -// ); - -// /** -// * @notice User's funds have been bonded -// * @param _bonder The user who is getting its tokens bonded -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` bonded -// */ -// event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice User's funds have been released -// * @param _beneficiary The user who is getting its tokens released -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` released -// */ -// event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Thrown when the account doesn't have enough balance to bond/withdraw -// * or not enough bonded to release/pay -// */ -// error AccountingExtension_InsufficientFunds(); - -// /** -// * @notice Thrown when the module bonding user tokens hasn't been approved by the user. -// */ -// error AccountingExtension_InsufficientAllowance(); - -// /** -// * @notice Thrown when an `onlyAllowedModule` function is called by something -// * else than a module being used in the corresponding request -// */ -// error AccountingExtension_UnauthorizedModule(); - -// /** -// * @notice Thrown when an `onlyParticipant` function is called with an address -// * that is not part of the request. -// */ -// error AccountingExtension_UnauthorizedUser(); - -// /*/////////////////////////////////////////////////////////////// -// VARIABLES -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Returns the interface for the Oracle contract -// */ -// function ORACLE() external view returns (IOracle _oracle); - -// /** -// * @notice Returns the amount of a token a user has bonded -// * @param _user The address of the user with bonded tokens -// * @param _bondToken The token bonded -// * @param _requestId The id of the request the user bonded for -// * @return _amount The amount of `_bondToken` bonded -// */ -// function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); - -// /** -// * @notice Returns the amount of a token a user has deposited -// * @param _user The address of the user with deposited tokens -// * @param _token The token deposited -// * @return _amount The amount of `_token` deposited -// */ -// function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Transfers tokens from a user and updates his virtual balance -// * @dev The user must have approved the accounting extension to transfer the tokens. -// * @param _token The address of the token being deposited -// * @param _amount The amount of `_token` to deposit -// */ -// function deposit(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows an user to withdraw deposited tokens -// * @param _token The address of the token being withdrawn -// * @param _amount The amount of `_token` to withdraw -// */ -// function withdraw(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to transfer bonded tokens from one user to another -// * @dev Only the virtual balances in the accounting extension are modified. The token contract -// * is not called nor its balances modified. -// * @param _requestId The id of the request handling the user's tokens -// * @param _payer The address of the user paying the tokens -// * @param _receiver The address of the user receiving the tokens -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` being transferred -// */ -// function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a valid module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// * @param _sender The address starting the propose call on the Oracle -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; - -// /** -// * @notice Allows a valid module to release a user's tokens -// * @param _bonder The address of the user to release tokens for -// * @param _requestId The id of the request where the tokens were bonded -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` to release -// */ -// function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a user to approve a module for bonding tokens -// * @param _module The address of the module to be approved -// */ -// function approveModule(address _module) external; - -// /** -// * @notice Allows a user to revoke a module's approval for bonding tokens -// * @param _module The address of the module to be revoked -// */ -// function revokeModule(address _module) external; - -// /** -// * @notice Returns a list of all modules a user has approved -// * @param _user The address of the user -// * @return _approvedModules The array of all modules approved by the user -// */ -// function approvedModules(address _user) external view returns (address[] memory _approvedModules); -// } +// 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'; + +/* + * @title AccountingExtension + * @notice Extension allowing users to deposit and bond funds + * to be used for payments and disputes. + */ +interface IAccountingExtension { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A user deposited tokens into the accounting extension + * @param _depositor The user who deposited the tokens + * @param _token The address of the token deposited by the user + * @param _amount The amount of `_token` deposited + */ + event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A user withdrew tokens from the accounting extension + * @param _withdrawer The user who withdrew the tokens + * @param _token The address of the token withdrawn by the user + * @param _amount The amount of `_token` withdrawn + */ + event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A payment between users has been made + * @param _beneficiary The user receiving the tokens + * @param _payer The user who is getting its tokens transferred + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` transferred + */ + event Paid( + bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount + ); + + /** + * @notice User's funds have been bonded + * @param _bonder The user who is getting its tokens bonded + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` bonded + */ + event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); + + /** + * @notice User's funds have been released + * @param _beneficiary The user who is getting its tokens released + * @param _token The address of the token being released + * @param _amount The amount of `_token` released + */ + event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Thrown when the account doesn't have enough balance to bond/withdraw + * or not enough bonded to release/pay + */ + error AccountingExtension_InsufficientFunds(); + + /** + * @notice Thrown when the module bonding user tokens hasn't been approved by the user. + */ + error AccountingExtension_InsufficientAllowance(); + + /** + * @notice Thrown when an `onlyAllowedModule` function is called by something + * else than a module being used in the corresponding request + */ + error AccountingExtension_UnauthorizedModule(); + + /** + * @notice Thrown when an `onlyParticipant` function is called with an address + * that is not part of the request. + */ + error AccountingExtension_UnauthorizedUser(); + + /*/////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the interface for the Oracle contract + */ + function ORACLE() external view returns (IOracle _oracle); + + /** + * @notice Returns the amount of a token a user has bonded + * @param _user The address of the user with bonded tokens + * @param _bondToken The token bonded + * @param _requestId The id of the request the user bonded for + * @return _amount The amount of `_bondToken` bonded + */ + function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); + + /** + * @notice Returns the amount of a token a user has deposited + * @param _user The address of the user with deposited tokens + * @param _token The token deposited + * @return _amount The amount of `_token` deposited + */ + function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Transfers tokens from a user and updates his virtual balance + * @dev The user must have approved the accounting extension to transfer the tokens. + * @param _token The address of the token being deposited + * @param _amount The amount of `_token` to deposit + */ + function deposit(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows an user to withdraw deposited tokens + * @param _token The address of the token being withdrawn + * @param _amount The amount of `_token` to withdraw + */ + function withdraw(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to transfer bonded tokens from one user to another + * @dev Only the virtual balances in the accounting extension are modified. The token contract + * is not called nor its balances modified. + * @param _requestId The id of the request handling the user's tokens + * @param _payer The address of the user paying the tokens + * @param _receiver The address of the user receiving the tokens + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` being transferred + */ + function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a valid module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + * @param _sender The address starting the propose call on the Oracle + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; + + /** + * @notice Allows a valid module to release a user's tokens + * @param _bonder The address of the user to release tokens for + * @param _requestId The id of the request where the tokens were bonded + * @param _token The address of the token being released + * @param _amount The amount of `_token` to release + */ + function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a user to approve a module for bonding tokens + * @param _module The address of the module to be approved + */ + function approveModule(address _module) external; + + /** + * @notice Allows a user to revoke a module's approval for bonding tokens + * @param _module The address of the module to be revoked + */ + function revokeModule(address _module) external; + + /** + * @notice Returns a list of all modules a user has approved + * @param _user The address of the user + * @return _approvedModules The array of all modules approved by the user + */ + function approvedModules(address _user) external view returns (address[] memory _approvedModules); +} diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 46874489..af32e439 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -1,56 +1,66 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; - -// import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; - -// contract Helpers is DSTestPlus { -// modifier assumeFuzzable(address _address) { -// _assumeFuzzable(_address); -// _; -// } - -// function _assumeFuzzable(address _address) internal pure { -// assumeNotForgeAddress(_address); -// assumeNotZeroAddress(_address); -// assumeNotPrecompile(_address); -// } - -// function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { -// vm.mockCall(_receiver, _calldata, _returned); -// vm.expectCall(_receiver, _calldata); -// } - -// function _getMockDispute( -// bytes32 _requestId, -// address _disputer, -// address _proposer -// ) internal view returns (IOracle.Dispute memory _dispute) { -// _dispute = IOracle.Dispute({ -// disputer: _disputer, -// responseId: bytes32('response'), -// proposer: _proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.None, -// createdAt: block.timestamp -// }); -// } - -// function _forBondDepositERC20( -// IAccountingExtension _accountingExtension, -// address _depositor, -// IERC20 _token, -// uint256 _depositAmount, -// uint256 _balanceIncrease -// ) internal { -// vm.assume(_balanceIncrease >= _depositAmount); -// deal(address(_token), _depositor, _balanceIncrease); -// vm.startPrank(_depositor); -// _token.approve(address(_accountingExtension), _depositAmount); -// _accountingExtension.deposit(_token, _depositAmount); -// vm.stopPrank(); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; + +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; + +contract Helpers is DSTestPlus { + modifier assumeFuzzable(address _address) { + _assumeFuzzable(_address); + _; + } + + function _assumeFuzzable(address _address) internal pure { + assumeNotForgeAddress(_address); + assumeNotZeroAddress(_address); + assumeNotPrecompile(_address); + } + + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + function _getMockDispute( + bytes32 _requestId, + address _disputer, + address _proposer + ) internal view returns (IOracle.Dispute memory _dispute) { + _dispute = IOracle.Dispute({ + disputer: _disputer, + responseId: bytes32('response'), + proposer: _proposer, + requestId: _requestId + }); + } + + function _forBondDepositERC20( + IAccountingExtension _accountingExtension, + address _depositor, + IERC20 _token, + uint256 _depositAmount, + uint256 _balanceIncrease + ) internal { + vm.assume(_balanceIncrease >= _depositAmount); + deal(address(_token), _depositor, _balanceIncrease); + vm.startPrank(_depositor); + _token.approve(address(_accountingExtension), _depositAmount); + _accountingExtension.deposit(_token, _depositAmount); + vm.stopPrank(); + } + + function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_response)); + } + + function _getId(IOracle.Request memory _request) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_request)); + } + + function _getId(IOracle.Dispute memory _dispute) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_dispute)); + } +} From 05f517562f1709585d6da2d776f9f749d0ed774f Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:23:58 +0400 Subject: [PATCH 07/53] feat: uncomment helpers --- .../extensions/IAccountingExtension.sol | 380 +++++++++--------- solidity/test/utils/Helpers.sol | 124 +++--- 2 files changed, 258 insertions(+), 246 deletions(-) diff --git a/solidity/interfaces/extensions/IAccountingExtension.sol b/solidity/interfaces/extensions/IAccountingExtension.sol index 5a42243e..74c9c9cd 100644 --- a/solidity/interfaces/extensions/IAccountingExtension.sol +++ b/solidity/interfaces/extensions/IAccountingExtension.sol @@ -1,190 +1,190 @@ -// // 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'; - -// /* -// * @title AccountingExtension -// * @notice Extension allowing users to deposit and bond funds -// * to be used for payments and disputes. -// */ -// interface IAccountingExtension { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice A user deposited tokens into the accounting extension -// * @param _depositor The user who deposited the tokens -// * @param _token The address of the token deposited by the user -// * @param _amount The amount of `_token` deposited -// */ -// event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A user withdrew tokens from the accounting extension -// * @param _withdrawer The user who withdrew the tokens -// * @param _token The address of the token withdrawn by the user -// * @param _amount The amount of `_token` withdrawn -// */ -// event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A payment between users has been made -// * @param _beneficiary The user receiving the tokens -// * @param _payer The user who is getting its tokens transferred -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` transferred -// */ -// event Paid( -// bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount -// ); - -// /** -// * @notice User's funds have been bonded -// * @param _bonder The user who is getting its tokens bonded -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` bonded -// */ -// event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice User's funds have been released -// * @param _beneficiary The user who is getting its tokens released -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` released -// */ -// event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Thrown when the account doesn't have enough balance to bond/withdraw -// * or not enough bonded to release/pay -// */ -// error AccountingExtension_InsufficientFunds(); - -// /** -// * @notice Thrown when the module bonding user tokens hasn't been approved by the user. -// */ -// error AccountingExtension_InsufficientAllowance(); - -// /** -// * @notice Thrown when an `onlyAllowedModule` function is called by something -// * else than a module being used in the corresponding request -// */ -// error AccountingExtension_UnauthorizedModule(); - -// /** -// * @notice Thrown when an `onlyParticipant` function is called with an address -// * that is not part of the request. -// */ -// error AccountingExtension_UnauthorizedUser(); - -// /*/////////////////////////////////////////////////////////////// -// VARIABLES -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Returns the interface for the Oracle contract -// */ -// function ORACLE() external view returns (IOracle _oracle); - -// /** -// * @notice Returns the amount of a token a user has bonded -// * @param _user The address of the user with bonded tokens -// * @param _bondToken The token bonded -// * @param _requestId The id of the request the user bonded for -// * @return _amount The amount of `_bondToken` bonded -// */ -// function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); - -// /** -// * @notice Returns the amount of a token a user has deposited -// * @param _user The address of the user with deposited tokens -// * @param _token The token deposited -// * @return _amount The amount of `_token` deposited -// */ -// function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Transfers tokens from a user and updates his virtual balance -// * @dev The user must have approved the accounting extension to transfer the tokens. -// * @param _token The address of the token being deposited -// * @param _amount The amount of `_token` to deposit -// */ -// function deposit(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows an user to withdraw deposited tokens -// * @param _token The address of the token being withdrawn -// * @param _amount The amount of `_token` to withdraw -// */ -// function withdraw(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to transfer bonded tokens from one user to another -// * @dev Only the virtual balances in the accounting extension are modified. The token contract -// * is not called nor its balances modified. -// * @param _requestId The id of the request handling the user's tokens -// * @param _payer The address of the user paying the tokens -// * @param _receiver The address of the user receiving the tokens -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` being transferred -// */ -// function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a valid module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// * @param _sender The address starting the propose call on the Oracle -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; - -// /** -// * @notice Allows a valid module to release a user's tokens -// * @param _bonder The address of the user to release tokens for -// * @param _requestId The id of the request where the tokens were bonded -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` to release -// */ -// function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a user to approve a module for bonding tokens -// * @param _module The address of the module to be approved -// */ -// function approveModule(address _module) external; - -// /** -// * @notice Allows a user to revoke a module's approval for bonding tokens -// * @param _module The address of the module to be revoked -// */ -// function revokeModule(address _module) external; - -// /** -// * @notice Returns a list of all modules a user has approved -// * @param _user The address of the user -// * @return _approvedModules The array of all modules approved by the user -// */ -// function approvedModules(address _user) external view returns (address[] memory _approvedModules); -// } +// 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'; + +/* + * @title AccountingExtension + * @notice Extension allowing users to deposit and bond funds + * to be used for payments and disputes. + */ +interface IAccountingExtension { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A user deposited tokens into the accounting extension + * @param _depositor The user who deposited the tokens + * @param _token The address of the token deposited by the user + * @param _amount The amount of `_token` deposited + */ + event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A user withdrew tokens from the accounting extension + * @param _withdrawer The user who withdrew the tokens + * @param _token The address of the token withdrawn by the user + * @param _amount The amount of `_token` withdrawn + */ + event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A payment between users has been made + * @param _beneficiary The user receiving the tokens + * @param _payer The user who is getting its tokens transferred + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` transferred + */ + event Paid( + bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount + ); + + /** + * @notice User's funds have been bonded + * @param _bonder The user who is getting its tokens bonded + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` bonded + */ + event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); + + /** + * @notice User's funds have been released + * @param _beneficiary The user who is getting its tokens released + * @param _token The address of the token being released + * @param _amount The amount of `_token` released + */ + event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Thrown when the account doesn't have enough balance to bond/withdraw + * or not enough bonded to release/pay + */ + error AccountingExtension_InsufficientFunds(); + + /** + * @notice Thrown when the module bonding user tokens hasn't been approved by the user. + */ + error AccountingExtension_InsufficientAllowance(); + + /** + * @notice Thrown when an `onlyAllowedModule` function is called by something + * else than a module being used in the corresponding request + */ + error AccountingExtension_UnauthorizedModule(); + + /** + * @notice Thrown when an `onlyParticipant` function is called with an address + * that is not part of the request. + */ + error AccountingExtension_UnauthorizedUser(); + + /*/////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the interface for the Oracle contract + */ + function ORACLE() external view returns (IOracle _oracle); + + /** + * @notice Returns the amount of a token a user has bonded + * @param _user The address of the user with bonded tokens + * @param _bondToken The token bonded + * @param _requestId The id of the request the user bonded for + * @return _amount The amount of `_bondToken` bonded + */ + function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); + + /** + * @notice Returns the amount of a token a user has deposited + * @param _user The address of the user with deposited tokens + * @param _token The token deposited + * @return _amount The amount of `_token` deposited + */ + function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Transfers tokens from a user and updates his virtual balance + * @dev The user must have approved the accounting extension to transfer the tokens. + * @param _token The address of the token being deposited + * @param _amount The amount of `_token` to deposit + */ + function deposit(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows an user to withdraw deposited tokens + * @param _token The address of the token being withdrawn + * @param _amount The amount of `_token` to withdraw + */ + function withdraw(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to transfer bonded tokens from one user to another + * @dev Only the virtual balances in the accounting extension are modified. The token contract + * is not called nor its balances modified. + * @param _requestId The id of the request handling the user's tokens + * @param _payer The address of the user paying the tokens + * @param _receiver The address of the user receiving the tokens + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` being transferred + */ + function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a valid module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + * @param _sender The address starting the propose call on the Oracle + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; + + /** + * @notice Allows a valid module to release a user's tokens + * @param _bonder The address of the user to release tokens for + * @param _requestId The id of the request where the tokens were bonded + * @param _token The address of the token being released + * @param _amount The amount of `_token` to release + */ + function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a user to approve a module for bonding tokens + * @param _module The address of the module to be approved + */ + function approveModule(address _module) external; + + /** + * @notice Allows a user to revoke a module's approval for bonding tokens + * @param _module The address of the module to be revoked + */ + function revokeModule(address _module) external; + + /** + * @notice Returns a list of all modules a user has approved + * @param _user The address of the user + * @return _approvedModules The array of all modules approved by the user + */ + function approvedModules(address _user) external view returns (address[] memory _approvedModules); +} diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 46874489..5bddd4bc 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -1,56 +1,68 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; - -// import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; - -// contract Helpers is DSTestPlus { -// modifier assumeFuzzable(address _address) { -// _assumeFuzzable(_address); -// _; -// } - -// function _assumeFuzzable(address _address) internal pure { -// assumeNotForgeAddress(_address); -// assumeNotZeroAddress(_address); -// assumeNotPrecompile(_address); -// } - -// function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { -// vm.mockCall(_receiver, _calldata, _returned); -// vm.expectCall(_receiver, _calldata); -// } - -// function _getMockDispute( -// bytes32 _requestId, -// address _disputer, -// address _proposer -// ) internal view returns (IOracle.Dispute memory _dispute) { -// _dispute = IOracle.Dispute({ -// disputer: _disputer, -// responseId: bytes32('response'), -// proposer: _proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.None, -// createdAt: block.timestamp -// }); -// } - -// function _forBondDepositERC20( -// IAccountingExtension _accountingExtension, -// address _depositor, -// IERC20 _token, -// uint256 _depositAmount, -// uint256 _balanceIncrease -// ) internal { -// vm.assume(_balanceIncrease >= _depositAmount); -// deal(address(_token), _depositor, _balanceIncrease); -// vm.startPrank(_depositor); -// _token.approve(address(_accountingExtension), _depositAmount); -// _accountingExtension.deposit(_token, _depositAmount); -// vm.stopPrank(); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; + +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; + +contract Helpers is DSTestPlus { + modifier assumeFuzzable(address _address) { + _assumeFuzzable(_address); + _; + } + + function _assumeFuzzable(address _address) internal pure { + assumeNotForgeAddress(_address); + assumeNotZeroAddress(_address); + assumeNotPrecompile(_address); + } + + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + function _getMockDispute( + bytes32 _requestId, + address _disputer, + address _proposer + ) internal view returns (IOracle.Dispute memory _dispute) { + _dispute = IOracle.Dispute({ + disputer: _disputer, + responseId: bytes32('response'), + proposer: _proposer, + requestId: _requestId, + status: IOracle.DisputeStatus.None, + createdAt: block.timestamp + }); + } + + function _forBondDepositERC20( + IAccountingExtension _accountingExtension, + address _depositor, + IERC20 _token, + uint256 _depositAmount, + uint256 _balanceIncrease + ) internal { + vm.assume(_balanceIncrease >= _depositAmount); + deal(address(_token), _depositor, _balanceIncrease); + vm.startPrank(_depositor); + _token.approve(address(_accountingExtension), _depositAmount); + _accountingExtension.deposit(_token, _depositAmount); + vm.stopPrank(); + } + + function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_response)); + } + + function _getId(IOracle.Request memory _request) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_request)); + } + + function _getId(IOracle.Dispute memory _dispute) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_dispute)); + } +} From ba18258edfbf77ec83eba749667ce43a8b939c4b Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 19:19:43 +0400 Subject: [PATCH 08/53] perf: optimize `HttpRequestModule` --- .../modules/request/http_request_module.md | 4 +- .../modules/request/HttpRequestModule.sol | 125 +++-- .../modules/request/IHttpRequestModule.sol | 124 +++-- .../modules/request/HttpRequestModule.t.sol | 498 +++++++----------- 4 files changed, 330 insertions(+), 421 deletions(-) diff --git a/docs/src/content/modules/request/http_request_module.md b/docs/src/content/modules/request/http_request_module.md index 7b4a8ebf..332e16d5 100644 --- a/docs/src/content/modules/request/http_request_module.md +++ b/docs/src/content/modules/request/http_request_module.md @@ -10,8 +10,8 @@ The `HttpRequestModule` is a contract that allows users to request HTTP calls. ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: This method decodes the data for a request given its ID. It returns the URL, HTTP method, body, accounting extension, payment token, and payment amount associated with the request. -- `finalizeRequest(bytes32 _requestId, address)`: This method finalizes a request by paying the proposer if there is a valid response, or releases the requester bond if no valid response was provided. +- `decodeRequestData(bytes calldata _data)`: This method decodes the data for a request given its ID. It returns the URL, HTTP method, body, accounting extension, payment token, and payment amount associated with the request. +- `finalizeRequest`: This method finalizes a request by paying the proposer if there is a valid response, or releases the requester bond if no valid response was provided. ### Request Parameters diff --git a/solidity/contracts/modules/request/HttpRequestModule.sol b/solidity/contracts/modules/request/HttpRequestModule.sol index ef8890fa..ad5324fc 100644 --- a/solidity/contracts/modules/request/HttpRequestModule.sol +++ b/solidity/contracts/modules/request/HttpRequestModule.sol @@ -1,63 +1,62 @@ -// // 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 {IHttpRequestModule} from '../../../interfaces/modules/request/IHttpRequestModule.sol'; - -// contract HttpRequestModule is Module, IHttpRequestModule { -// constructor(IOracle _oracle) Module(_oracle) {} - -// /// @inheritdoc IModule -// function moduleName() public pure returns (string memory _moduleName) { -// _moduleName = 'HttpRequestModule'; -// } - -// /// @inheritdoc IHttpRequestModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } - -// /** -// * @notice Bonds the requester tokens to use as payment for the response proposer. -// */ -// 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 IHttpRequestModule -// function finalizeRequest( -// bytes32 _requestId, -// address _finalizer -// ) external override(IHttpRequestModule, 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); -// } -// } +// 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 {IHttpRequestModule} from '../../../interfaces/modules/request/IHttpRequestModule.sol'; + +contract HttpRequestModule is Module, IHttpRequestModule { + constructor(IOracle _oracle) Module(_oracle) {} + + /// @inheritdoc IModule + function moduleName() public pure returns (string memory _moduleName) { + _moduleName = 'HttpRequestModule'; + } + + /// @inheritdoc IHttpRequestModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } + + /// @inheritdoc IHttpRequestModule + function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external { + RequestParameters memory _params = decodeRequestData(_data); + + _params.accountingExtension.bond({ + _bonder: _requester, + _requestId: _requestId, + _token: _params.paymentToken, + _amount: _params.paymentAmount + }); + } + + /// @inheritdoc IHttpRequestModule + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external override(IHttpRequestModule, 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/IHttpRequestModule.sol b/solidity/interfaces/modules/request/IHttpRequestModule.sol index 5c28a4c4..9a01a797 100644 --- a/solidity/interfaces/modules/request/IHttpRequestModule.sol +++ b/solidity/interfaces/modules/request/IHttpRequestModule.sol @@ -1,65 +1,75 @@ -// // 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 {IAccountingExtension} from '../../../interfaces/extensions/IAccountingExtension.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'; -// /* -// * @title HttpRequestModule -// * @notice Module allowing users to request HTTP calls -// */ -// interface IHttpRequestModule is IRequestModule { -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ +/* + * @title HttpRequestModule + * @notice Module allowing users to request HTTP calls + */ +interface IHttpRequestModule is IRequestModule { + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Parameters of the request as stored in the module -// * @param url The url to make the request to -// * @param method The HTTP method to use for the request -// * @param body The HTTP body to use for the request -// * @param accountingExtension The accounting extension used to bond and release tokens -// * @param paymentToken The token used to pay for the request -// * @param paymentAmount The amount of tokens to pay for the request -// */ -// struct RequestParameters { -// string url; -// string body; -// HttpMethod method; -// IAccountingExtension accountingExtension; -// IERC20 paymentToken; -// uint256 paymentAmount; -// } + /** + * @notice Parameters of the request as stored in the module + * @param url The url to make the request to + * @param method The HTTP method to use for the request + * @param body The HTTP body to use for the request + * @param accountingExtension The accounting extension used to bond and release tokens + * @param paymentToken The token used to pay for the request + * @param paymentAmount The amount of tokens to pay for the request + */ + struct RequestParameters { + string url; + string body; + HttpMethod method; + IAccountingExtension accountingExtension; + IERC20 paymentToken; + uint256 paymentAmount; + } -// /*/////////////////////////////////////////////////////////////// -// ENUMS -// //////////////////////////////////////////////////////////////*/ + /*/////////////////////////////////////////////////////////////// + ENUMS + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Available HTTP methods -// */ -// enum HttpMethod { -// GET, -// POST -// } + /** + * @notice Available HTTP methods + */ + enum HttpMethod { + GET, + POST + } -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ -// /** -// * @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 proposer if there is a valid response -// * or releases the requester bond if no valid response was provided -// * @param _requestId The ID of the request -// */ -// function finalizeRequest(bytes32 _requestId, address) external; -// } + /// @inheritdoc IRequestModule + 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/HttpRequestModule.t.sol b/solidity/test/unit/modules/request/HttpRequestModule.t.sol index a053e9fe..e7e300e7 100644 --- a/solidity/test/unit/modules/request/HttpRequestModule.t.sol +++ b/solidity/test/unit/modules/request/HttpRequestModule.t.sol @@ -1,299 +1,199 @@ -// // 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 {HttpRequestModule, IHttpRequestModule} from '../../../../contracts/modules/request/HttpRequestModule.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_HttpRequestModule is HttpRequestModule { -// constructor(IOracle _oracle) HttpRequestModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } -// } - -// /** -// * @title HTTP Request Module Unit tests -// */ -// contract BaseTest is Test, Helpers { -// // Mock request data -// string public constant URL = 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd'; -// IHttpRequestModule.HttpMethod public constant METHOD = IHttpRequestModule.HttpMethod.GET; -// string public constant BODY = '69420'; - -// // Mock token -// IERC20 public immutable TOKEN = IERC20(makeAddr('ERC20')); -// // The target contract -// ForTest_HttpRequestModule public httpRequestModule; -// // A mock oracle -// IOracle public oracle; -// // A mock accounting extension -// IAccountingExtension public accounting = IAccountingExtension(makeAddr('accounting')); - -// 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'); - -// httpRequestModule = new ForTest_HttpRequestModule(oracle); -// } -// } - -// contract HttpRequestModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(httpRequestModule.moduleName(), 'HttpRequestModule'); -// } - -// /** -// * @notice Test that the decodeRequestData function returns the correct values -// */ -// function test_decodeRequestData(bytes32 _requestId, uint256 _amount, IERC20 _token) public { -// bytes memory _requestData = abi.encode( -// IHttpRequestModule.RequestParameters({ -// url: URL, -// method: METHOD, -// body: BODY, -// accountingExtension: accounting, -// paymentToken: _token, -// paymentAmount: _amount -// }) -// ); - -// // Set the request data -// httpRequestModule.forTest_setRequestData(_requestId, _requestData); - -// // Decode the given request data -// IHttpRequestModule.RequestParameters memory _params = httpRequestModule.decodeRequestData(_requestId); - -// // Check: decoded values match original values? -// assertEq(_params.url, URL); -// assertEq(uint256(_params.method), uint256(METHOD)); -// assertEq(_params.body, BODY); -// assertEq(address(_params.accountingExtension), address(accounting)); -// assertEq(address(_params.paymentToken), address(_token)); -// assertEq(_params.paymentAmount, _amount); -// } -// } - -// contract HttpRequestModule_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, -// uint256 _amount, -// IERC20 _token -// ) public { -// bytes memory _requestData = abi.encode( -// IHttpRequestModule.RequestParameters({ -// url: URL, -// method: METHOD, -// body: BODY, -// accountingExtension: accounting, -// paymentToken: _token, -// paymentAmount: _amount -// }) -// ); - -// 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, _token, _amount), -// abi.encode(true) -// ); - -// vm.prank(address(oracle)); -// httpRequestModule.setupRequest(_requestId, _requestData); -// } -// } - -// contract HttpRequestModule_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, -// uint256 _amount, -// IERC20 _token -// ) public { -// _amount = bound(_amount, 0, type(uint248).max); - -// // Use the correct accounting parameters -// bytes memory _requestData = abi.encode( -// IHttpRequestModule.RequestParameters({ -// url: URL, -// method: METHOD, -// body: BODY, -// accountingExtension: accounting, -// paymentToken: _token, -// paymentAmount: _amount -// }) -// ); - -// IOracle.Request memory _fullRequest; -// _fullRequest.requester = _requester; - -// IOracle.Response memory _fullResponse; -// _fullResponse.proposer = _proposer; -// _fullResponse.createdAt = block.timestamp; - -// // Set the request data -// httpRequestModule.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 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, _token, _amount)), -// abi.encode() -// ); - -// vm.startPrank(address(oracle)); -// httpRequestModule.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, _token, _amount)), -// abi.encode(true) -// ); - -// httpRequestModule.finalizeRequest(_requestId, address(this)); -// } - -// function test_emitsEvent( -// bytes32 _requestId, -// address _requester, -// address _proposer, -// uint256 _amount, -// IERC20 _token -// ) public { -// // Use the correct accounting parameters -// bytes memory _requestData = abi.encode( -// IHttpRequestModule.RequestParameters({ -// url: URL, -// method: METHOD, -// body: BODY, -// accountingExtension: accounting, -// paymentToken: _token, -// paymentAmount: _amount -// }) -// ); - -// IOracle.Request memory _fullRequest; -// _fullRequest.requester = _requester; - -// IOracle.Response memory _fullResponse; -// _fullResponse.proposer = _proposer; -// _fullResponse.createdAt = block.timestamp; - -// // Set the request data -// httpRequestModule.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 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, _token, _amount)), -// abi.encode() -// ); - -// vm.startPrank(address(oracle)); -// httpRequestModule.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, _token, _amount)), -// abi.encode(true) -// ); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(httpRequestModule)); -// emit RequestFinalized(_requestId, address(this)); - -// httpRequestModule.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); -// httpRequestModule.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 {HttpRequestModule, IHttpRequestModule} from '../../../../contracts/modules/request/HttpRequestModule.sol'; + +import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; + +/** + * @title HTTP Request Module Unit tests + */ +contract BaseTest is Test, Helpers { + // Mock request data + string public constant URL = 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd'; + IHttpRequestModule.HttpMethod public constant METHOD = IHttpRequestModule.HttpMethod.GET; + string public constant BODY = '69420'; + + // Mock token + IERC20 public immutable TOKEN = IERC20(makeAddr('ERC20')); + // The target contract + HttpRequestModule public httpRequestModule; + // A mock oracle + IOracle public oracle; + // A mock accounting extension + IAccountingExtension public accounting = IAccountingExtension(makeAddr('accounting')); + IOracle.Response public mockResponse; + address internal _proposer = makeAddr('proposer'); + bytes32 public mockId = bytes32('69'); + + 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'); + + httpRequestModule = new HttpRequestModule(oracle); + mockResponse = IOracle.Response({proposer: _proposer, requestId: mockId, response: bytes('')}); + } +} + +contract HttpRequestModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(httpRequestModule.moduleName(), 'HttpRequestModule'); + } + + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData(bytes32 _requestId, uint256 _amount, IERC20 _token) public { + bytes memory _requestData = abi.encode( + IHttpRequestModule.RequestParameters({ + url: URL, + method: METHOD, + body: BODY, + accountingExtension: accounting, + paymentToken: _token, + paymentAmount: _amount + }) + ); + + // Decode the given request data + IHttpRequestModule.RequestParameters memory _params = httpRequestModule.decodeRequestData(_requestData); + + // Check: decoded values match original values? + assertEq(_params.url, URL); + assertEq(uint256(_params.method), uint256(METHOD)); + assertEq(_params.body, BODY); + assertEq(address(_params.accountingExtension), address(accounting)); + assertEq(address(_params.paymentToken), address(_token)); + assertEq(_params.paymentAmount, _amount); + } +} + +contract HttpRequestModule_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, + uint256 _amount, + IERC20 _token, + IOracle.Request calldata _request + ) public { + _amount = bound(_amount, 0, type(uint248).max); + + // Use the correct accounting parameters + bytes memory _requestData = abi.encode( + IHttpRequestModule.RequestParameters({ + url: URL, + method: METHOD, + body: BODY, + accountingExtension: accounting, + paymentToken: _token, + paymentAmount: _amount + }) + ); + + // Mock and expect IAccountingExtension.pay to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _token, _amount)), + abi.encode() + ); + + vm.startPrank(address(oracle)); + httpRequestModule.finalizeRequest(_request, mockResponse, address(oracle)); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _token, _amount)), + abi.encode(true) + ); + + httpRequestModule.finalizeRequest(_request, mockResponse, address(this)); + } + + function test_emitsEvent( + bytes32 _requestId, + address _requester, + address _proposer, + uint256 _amount, + IERC20 _token, + IOracle.Request calldata _request + ) public { + // Use the correct accounting parameters + bytes memory _requestData = abi.encode( + IHttpRequestModule.RequestParameters({ + url: URL, + method: METHOD, + body: BODY, + accountingExtension: accounting, + paymentToken: _token, + paymentAmount: _amount + }) + ); + + // Mock and expect IAccountingExtension.pay to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _token, _amount)), + abi.encode() + ); + + vm.startPrank(address(oracle)); + httpRequestModule.finalizeRequest(_request, mockResponse, address(oracle)); + + // Update mock call to return the response's createdAt + _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, (_requester, _requestId, _token, _amount)), + abi.encode(true) + ); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(httpRequestModule)); + emit RequestFinalized(_requestId, address(this)); + + httpRequestModule.finalizeRequest(_request, mockResponse, address(this)); + } + + /** + * @notice Test that the finalizeRequest reverts if caller is not the oracle + */ + function test_revertsIfWrongCaller(bytes32 _requestId, 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); + httpRequestModule.finalizeRequest(_request, mockResponse, address(_caller)); + } +} From 6f98e82f1c5d551755b0730af2525d58c54908f7 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 19:19:54 +0400 Subject: [PATCH 09/53] feat: fix helpers --- solidity/test/utils/Helpers.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 5bddd4bc..af32e439 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -33,9 +33,7 @@ contract Helpers is DSTestPlus { disputer: _disputer, responseId: bytes32('response'), proposer: _proposer, - requestId: _requestId, - status: IOracle.DisputeStatus.None, - createdAt: block.timestamp + requestId: _requestId }); } From 6e3e78be597ba5c6a11ee9efeecd9e8fb2849c46 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 19:26:31 +0400 Subject: [PATCH 10/53] feat: uncomment helpers --- .../extensions/IAccountingExtension.sol | 380 +++++++++--------- solidity/test/utils/Helpers.sol | 122 +++--- 2 files changed, 256 insertions(+), 246 deletions(-) diff --git a/solidity/interfaces/extensions/IAccountingExtension.sol b/solidity/interfaces/extensions/IAccountingExtension.sol index 5a42243e..74c9c9cd 100644 --- a/solidity/interfaces/extensions/IAccountingExtension.sol +++ b/solidity/interfaces/extensions/IAccountingExtension.sol @@ -1,190 +1,190 @@ -// // 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'; - -// /* -// * @title AccountingExtension -// * @notice Extension allowing users to deposit and bond funds -// * to be used for payments and disputes. -// */ -// interface IAccountingExtension { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice A user deposited tokens into the accounting extension -// * @param _depositor The user who deposited the tokens -// * @param _token The address of the token deposited by the user -// * @param _amount The amount of `_token` deposited -// */ -// event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A user withdrew tokens from the accounting extension -// * @param _withdrawer The user who withdrew the tokens -// * @param _token The address of the token withdrawn by the user -// * @param _amount The amount of `_token` withdrawn -// */ -// event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A payment between users has been made -// * @param _beneficiary The user receiving the tokens -// * @param _payer The user who is getting its tokens transferred -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` transferred -// */ -// event Paid( -// bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount -// ); - -// /** -// * @notice User's funds have been bonded -// * @param _bonder The user who is getting its tokens bonded -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` bonded -// */ -// event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice User's funds have been released -// * @param _beneficiary The user who is getting its tokens released -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` released -// */ -// event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Thrown when the account doesn't have enough balance to bond/withdraw -// * or not enough bonded to release/pay -// */ -// error AccountingExtension_InsufficientFunds(); - -// /** -// * @notice Thrown when the module bonding user tokens hasn't been approved by the user. -// */ -// error AccountingExtension_InsufficientAllowance(); - -// /** -// * @notice Thrown when an `onlyAllowedModule` function is called by something -// * else than a module being used in the corresponding request -// */ -// error AccountingExtension_UnauthorizedModule(); - -// /** -// * @notice Thrown when an `onlyParticipant` function is called with an address -// * that is not part of the request. -// */ -// error AccountingExtension_UnauthorizedUser(); - -// /*/////////////////////////////////////////////////////////////// -// VARIABLES -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Returns the interface for the Oracle contract -// */ -// function ORACLE() external view returns (IOracle _oracle); - -// /** -// * @notice Returns the amount of a token a user has bonded -// * @param _user The address of the user with bonded tokens -// * @param _bondToken The token bonded -// * @param _requestId The id of the request the user bonded for -// * @return _amount The amount of `_bondToken` bonded -// */ -// function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); - -// /** -// * @notice Returns the amount of a token a user has deposited -// * @param _user The address of the user with deposited tokens -// * @param _token The token deposited -// * @return _amount The amount of `_token` deposited -// */ -// function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Transfers tokens from a user and updates his virtual balance -// * @dev The user must have approved the accounting extension to transfer the tokens. -// * @param _token The address of the token being deposited -// * @param _amount The amount of `_token` to deposit -// */ -// function deposit(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows an user to withdraw deposited tokens -// * @param _token The address of the token being withdrawn -// * @param _amount The amount of `_token` to withdraw -// */ -// function withdraw(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to transfer bonded tokens from one user to another -// * @dev Only the virtual balances in the accounting extension are modified. The token contract -// * is not called nor its balances modified. -// * @param _requestId The id of the request handling the user's tokens -// * @param _payer The address of the user paying the tokens -// * @param _receiver The address of the user receiving the tokens -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` being transferred -// */ -// function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a valid module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// * @param _sender The address starting the propose call on the Oracle -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; - -// /** -// * @notice Allows a valid module to release a user's tokens -// * @param _bonder The address of the user to release tokens for -// * @param _requestId The id of the request where the tokens were bonded -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` to release -// */ -// function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a user to approve a module for bonding tokens -// * @param _module The address of the module to be approved -// */ -// function approveModule(address _module) external; - -// /** -// * @notice Allows a user to revoke a module's approval for bonding tokens -// * @param _module The address of the module to be revoked -// */ -// function revokeModule(address _module) external; - -// /** -// * @notice Returns a list of all modules a user has approved -// * @param _user The address of the user -// * @return _approvedModules The array of all modules approved by the user -// */ -// function approvedModules(address _user) external view returns (address[] memory _approvedModules); -// } +// 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'; + +/* + * @title AccountingExtension + * @notice Extension allowing users to deposit and bond funds + * to be used for payments and disputes. + */ +interface IAccountingExtension { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A user deposited tokens into the accounting extension + * @param _depositor The user who deposited the tokens + * @param _token The address of the token deposited by the user + * @param _amount The amount of `_token` deposited + */ + event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A user withdrew tokens from the accounting extension + * @param _withdrawer The user who withdrew the tokens + * @param _token The address of the token withdrawn by the user + * @param _amount The amount of `_token` withdrawn + */ + event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A payment between users has been made + * @param _beneficiary The user receiving the tokens + * @param _payer The user who is getting its tokens transferred + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` transferred + */ + event Paid( + bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount + ); + + /** + * @notice User's funds have been bonded + * @param _bonder The user who is getting its tokens bonded + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` bonded + */ + event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); + + /** + * @notice User's funds have been released + * @param _beneficiary The user who is getting its tokens released + * @param _token The address of the token being released + * @param _amount The amount of `_token` released + */ + event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Thrown when the account doesn't have enough balance to bond/withdraw + * or not enough bonded to release/pay + */ + error AccountingExtension_InsufficientFunds(); + + /** + * @notice Thrown when the module bonding user tokens hasn't been approved by the user. + */ + error AccountingExtension_InsufficientAllowance(); + + /** + * @notice Thrown when an `onlyAllowedModule` function is called by something + * else than a module being used in the corresponding request + */ + error AccountingExtension_UnauthorizedModule(); + + /** + * @notice Thrown when an `onlyParticipant` function is called with an address + * that is not part of the request. + */ + error AccountingExtension_UnauthorizedUser(); + + /*/////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the interface for the Oracle contract + */ + function ORACLE() external view returns (IOracle _oracle); + + /** + * @notice Returns the amount of a token a user has bonded + * @param _user The address of the user with bonded tokens + * @param _bondToken The token bonded + * @param _requestId The id of the request the user bonded for + * @return _amount The amount of `_bondToken` bonded + */ + function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); + + /** + * @notice Returns the amount of a token a user has deposited + * @param _user The address of the user with deposited tokens + * @param _token The token deposited + * @return _amount The amount of `_token` deposited + */ + function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Transfers tokens from a user and updates his virtual balance + * @dev The user must have approved the accounting extension to transfer the tokens. + * @param _token The address of the token being deposited + * @param _amount The amount of `_token` to deposit + */ + function deposit(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows an user to withdraw deposited tokens + * @param _token The address of the token being withdrawn + * @param _amount The amount of `_token` to withdraw + */ + function withdraw(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to transfer bonded tokens from one user to another + * @dev Only the virtual balances in the accounting extension are modified. The token contract + * is not called nor its balances modified. + * @param _requestId The id of the request handling the user's tokens + * @param _payer The address of the user paying the tokens + * @param _receiver The address of the user receiving the tokens + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` being transferred + */ + function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a valid module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + * @param _sender The address starting the propose call on the Oracle + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; + + /** + * @notice Allows a valid module to release a user's tokens + * @param _bonder The address of the user to release tokens for + * @param _requestId The id of the request where the tokens were bonded + * @param _token The address of the token being released + * @param _amount The amount of `_token` to release + */ + function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a user to approve a module for bonding tokens + * @param _module The address of the module to be approved + */ + function approveModule(address _module) external; + + /** + * @notice Allows a user to revoke a module's approval for bonding tokens + * @param _module The address of the module to be revoked + */ + function revokeModule(address _module) external; + + /** + * @notice Returns a list of all modules a user has approved + * @param _user The address of the user + * @return _approvedModules The array of all modules approved by the user + */ + function approvedModules(address _user) external view returns (address[] memory _approvedModules); +} diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 46874489..af32e439 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -1,56 +1,66 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; - -// import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; - -// contract Helpers is DSTestPlus { -// modifier assumeFuzzable(address _address) { -// _assumeFuzzable(_address); -// _; -// } - -// function _assumeFuzzable(address _address) internal pure { -// assumeNotForgeAddress(_address); -// assumeNotZeroAddress(_address); -// assumeNotPrecompile(_address); -// } - -// function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { -// vm.mockCall(_receiver, _calldata, _returned); -// vm.expectCall(_receiver, _calldata); -// } - -// function _getMockDispute( -// bytes32 _requestId, -// address _disputer, -// address _proposer -// ) internal view returns (IOracle.Dispute memory _dispute) { -// _dispute = IOracle.Dispute({ -// disputer: _disputer, -// responseId: bytes32('response'), -// proposer: _proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.None, -// createdAt: block.timestamp -// }); -// } - -// function _forBondDepositERC20( -// IAccountingExtension _accountingExtension, -// address _depositor, -// IERC20 _token, -// uint256 _depositAmount, -// uint256 _balanceIncrease -// ) internal { -// vm.assume(_balanceIncrease >= _depositAmount); -// deal(address(_token), _depositor, _balanceIncrease); -// vm.startPrank(_depositor); -// _token.approve(address(_accountingExtension), _depositAmount); -// _accountingExtension.deposit(_token, _depositAmount); -// vm.stopPrank(); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; + +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; + +contract Helpers is DSTestPlus { + modifier assumeFuzzable(address _address) { + _assumeFuzzable(_address); + _; + } + + function _assumeFuzzable(address _address) internal pure { + assumeNotForgeAddress(_address); + assumeNotZeroAddress(_address); + assumeNotPrecompile(_address); + } + + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + function _getMockDispute( + bytes32 _requestId, + address _disputer, + address _proposer + ) internal view returns (IOracle.Dispute memory _dispute) { + _dispute = IOracle.Dispute({ + disputer: _disputer, + responseId: bytes32('response'), + proposer: _proposer, + requestId: _requestId + }); + } + + function _forBondDepositERC20( + IAccountingExtension _accountingExtension, + address _depositor, + IERC20 _token, + uint256 _depositAmount, + uint256 _balanceIncrease + ) internal { + vm.assume(_balanceIncrease >= _depositAmount); + deal(address(_token), _depositor, _balanceIncrease); + vm.startPrank(_depositor); + _token.approve(address(_accountingExtension), _depositAmount); + _accountingExtension.deposit(_token, _depositAmount); + vm.stopPrank(); + } + + function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_response)); + } + + function _getId(IOracle.Request memory _request) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_request)); + } + + function _getId(IOracle.Dispute memory _dispute) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_dispute)); + } +} From ddf6f72f8c3a34459e73dbb784f7bf3822ecc4ec Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 20:08:34 +0400 Subject: [PATCH 11/53] feat: update `prophet-core` package --- package.json | 2 +- yarn.lock | 72 +++++++++++++++++++++++++--------------------------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index b7700bc1..e85440a1 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "package.json": "sort-package-json" }, "dependencies": { - "@defi-wonderland/prophet-core-contracts": "0.0.0-d05a00d0", + "@defi-wonderland/prophet-core-contracts": "0.0.0-48b0248d", "@defi-wonderland/solidity-utils": "0.0.0-3e9c8e8b", "@openzeppelin/contracts": "^4.9.3", "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", diff --git a/yarn.lock b/yarn.lock index c61b7709..a6dd1042 100644 --- a/yarn.lock +++ b/yarn.lock @@ -192,10 +192,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@defi-wonderland/prophet-core-contracts@0.0.0-d05a00d0": - version "0.0.0-d05a00d0" - resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-d05a00d0.tgz#1357d917fe46a5a12faa67f557e990255dda14fd" - integrity sha512-F/y0r/qDLFACzsN7Y2VRAPIS9Yhx2btU/m7cQT7T84TbIxAmBGVw6/7nb+HeIbXh+QDO90RP6vHAdQOow/q1Xw== +"@defi-wonderland/prophet-core-contracts@0.0.0-48b0248d": + version "0.0.0-48b0248d" + resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-48b0248d.tgz#16d0473360074f17b66199c8e3660b71a3d72ad4" + integrity sha512-bEufdaPkLcg1VuYpTWRB5Xf4pmpV3wi0487taGI4A+YtwhMsIh9ZCNPdgWssqLvIfMPlB4FdGOM936l1yqAKYQ== dependencies: "@defi-wonderland/solidity-utils" "0.0.0-3e9c8e8b" "@openzeppelin/contracts" "^4.9.3" @@ -324,9 +324,9 @@ ts-essentials "^7.0.1" "@types/minimist@^1.2.0": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.4.tgz#81f886786411c45bba3f33e781ab48bd56bfca2e" - integrity sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@20.5.1": version "20.5.1" @@ -334,9 +334,9 @@ integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== "@types/normalize-package-data@^2.4.0": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz#291c243e4b94dbfbc0c0ee26b7666f1d5c030e2c" - integrity sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/prettier@^2.1.1": version "2.7.3" @@ -357,9 +357,9 @@ acorn-jsx@^5.0.0: integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== acorn@^6.0.7: version "6.4.2" @@ -367,9 +367,9 @@ acorn@^6.0.7: integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^8.4.1: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== add-stream@^1.0.0: version "1.0.0" @@ -1181,14 +1181,10 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -"ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": - version "1.0.0" - resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" - -"ds-test@https://github.com/dapphub/ds-test": +"ds-test@git+https://github.com/dapphub/ds-test.git", "ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" uid e282159d5170298eb2455a6c05280ab5a73a4ef0 - resolved "https://github.com/dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0" + resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" "ds-test@https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" @@ -1457,9 +1453,9 @@ fast-diff@^1.1.2, fast-diff@^1.2.0: integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== fast-glob@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -1570,14 +1566,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +"forge-std@git+https://github.com/foundry-rs/forge-std.git": + version "1.7.1" + resolved "git+https://github.com/foundry-rs/forge-std.git#267acd30a625086b3f16e1a28cfe0c5097fa46b8" + "forge-std@git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e": version "1.5.6" resolved "git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e" -"forge-std@https://github.com/foundry-rs/forge-std": - version "1.7.1" - resolved "https://github.com/foundry-rs/forge-std#267acd30a625086b3f16e1a28cfe0c5097fa46b8" - "forge-std@https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421": version "1.7.1" resolved "https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421" @@ -2858,9 +2854,9 @@ progress@^2.0.0: integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== q@^1.5.1: version "1.5.1" @@ -3787,9 +3783,9 @@ universalify@^0.1.0: integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== uri-js@^4.2.2: version "4.4.1" @@ -3916,9 +3912,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^2.2.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" - integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" From 4c2e6667837410d5819d74ff76c5d972bf516585 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 20:19:12 +0400 Subject: [PATCH 12/53] perf: optimize `ArbitratorModule` --- .../modules/resolution/arbitrator_module.md | 2 +- .../modules/resolution/ArbitratorModule.sol | 131 ++-- .../modules/resolution/IArbitratorModule.sol | 164 +++-- .../modules/resolution/ArbitratorModule.t.sol | 609 ++++++++---------- 4 files changed, 444 insertions(+), 462 deletions(-) diff --git a/docs/src/content/modules/resolution/arbitrator_module.md b/docs/src/content/modules/resolution/arbitrator_module.md index b0476171..dac2c04c 100644 --- a/docs/src/content/modules/resolution/arbitrator_module.md +++ b/docs/src/content/modules/resolution/arbitrator_module.md @@ -14,7 +14,7 @@ The Arbitrator Module is a part of the dispute resolution system. It allows an e - `isValid(bytes32 _disputeId)`: Indicates whether the dispute has been arbitrated. - `startResolution(bytes32 _disputeId)`: Starts the arbitration process by calling `resolve` on the arbitrator and flags the dispute as `Active`. - `resolveDispute(bytes32 _disputeId)`: Resolves the dispute by getting the answer from the arbitrator and notifying the oracle. -- `decodeRequestData(bytes32 _requestId)`: Returns the decoded data for a request. +- `decodeRequestData(bytes calldata _data)`: Returns the decoded data for a request. ### Request Parameters diff --git a/solidity/contracts/modules/resolution/ArbitratorModule.sol b/solidity/contracts/modules/resolution/ArbitratorModule.sol index daf419fc..0f4df46a 100644 --- a/solidity/contracts/modules/resolution/ArbitratorModule.sol +++ b/solidity/contracts/modules/resolution/ArbitratorModule.sol @@ -1,62 +1,69 @@ -// // 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 {IArbitratorModule} from '../../../interfaces/modules/resolution/IArbitratorModule.sol'; -// import {IArbitrator} from '../../../interfaces/IArbitrator.sol'; - -// contract ArbitratorModule is Module, IArbitratorModule { -// /** -// * @notice The status of all disputes -// */ -// mapping(bytes32 _disputeId => ArbitrationStatus _status) internal _disputeData; - -// constructor(IOracle _oracle) Module(_oracle) {} - -// /// @inheritdoc IModule -// function moduleName() external pure returns (string memory _moduleName) { -// return 'ArbitratorModule'; -// } - -// /// @inheritdoc IArbitratorModule -// function decodeRequestData(bytes32 _requestId) public view returns (address _arbitrator) { -// _arbitrator = abi.decode(requestData[_requestId], (address)); -// } - -// /// @inheritdoc IArbitratorModule -// function getStatus(bytes32 _disputeId) external view returns (ArbitrationStatus _disputeStatus) { -// _disputeStatus = _disputeData[_disputeId]; -// } - -// /// @inheritdoc IArbitratorModule -// function startResolution(bytes32 _disputeId) external onlyOracle { -// IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId); - -// address _arbitrator = abi.decode(requestData[_dispute.requestId], (address)); -// if (_arbitrator == address(0)) revert ArbitratorModule_InvalidArbitrator(); - -// _disputeData[_disputeId] = ArbitrationStatus.Active; -// IArbitrator(_arbitrator).resolve(_disputeId); - -// emit ResolutionStarted(_dispute.requestId, _disputeId); -// } - -// /// @inheritdoc IArbitratorModule -// function resolveDispute(bytes32 _disputeId) external onlyOracle { -// IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId); -// if (_dispute.status != IOracle.DisputeStatus.Escalated) revert ArbitratorModule_InvalidDisputeId(); - -// address _arbitrator = abi.decode(requestData[_dispute.requestId], (address)); -// IOracle.DisputeStatus _status = IArbitrator(_arbitrator).getAnswer(_disputeId); - -// if (_status <= IOracle.DisputeStatus.Escalated) revert ArbitratorModule_InvalidResolutionStatus(); -// _disputeData[_disputeId] = ArbitrationStatus.Resolved; - -// ORACLE.updateDisputeStatus(_disputeId, _status); - -// emit DisputeResolved(_dispute.requestId, _disputeId, _status); -// } -// } +// 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 {IArbitratorModule} from '../../../interfaces/modules/resolution/IArbitratorModule.sol'; +import {IArbitrator} from '../../../interfaces/IArbitrator.sol'; + +contract ArbitratorModule is Module, IArbitratorModule { + /** + * @notice The status of all disputes + */ + mapping(bytes32 _disputeId => ArbitrationStatus _status) internal _disputeData; + + constructor(IOracle _oracle) Module(_oracle) {} + + /// @inheritdoc IModule + function moduleName() external pure returns (string memory _moduleName) { + return 'ArbitratorModule'; + } + + /// @inheritdoc IArbitratorModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } + + /// @inheritdoc IArbitratorModule + function getStatus(bytes32 _disputeId) external view returns (ArbitrationStatus _disputeStatus) { + _disputeStatus = _disputeData[_disputeId]; + } + + /// @inheritdoc IArbitratorModule + function startResolution( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.resolutionModuleData); + if (_params.arbitrator == address(0)) revert ArbitratorModule_InvalidArbitrator(); + + _disputeData[_disputeId] = ArbitrationStatus.Active; + IArbitrator(_params.arbitrator).resolve(_disputeId); + + emit ResolutionStarted(_dispute.requestId, _disputeId); + } + + /// @inheritdoc IArbitratorModule + function resolveDispute( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.Escalated) revert ArbitratorModule_InvalidDisputeId(); + + RequestParameters memory _params = decodeRequestData(_request.resolutionModuleData); + IOracle.DisputeStatus _status = IArbitrator(_params.arbitrator).getAnswer(_disputeId); + + if (_status <= IOracle.DisputeStatus.Escalated) revert ArbitratorModule_InvalidResolutionStatus(); + _disputeData[_disputeId] = ArbitrationStatus.Resolved; + + ORACLE.updateDisputeStatus(_request, _response, _dispute, _status); + + emit DisputeResolved(_dispute.requestId, _disputeId, _status); + } +} diff --git a/solidity/interfaces/modules/resolution/IArbitratorModule.sol b/solidity/interfaces/modules/resolution/IArbitratorModule.sol index b5bf211b..17e9747d 100644 --- a/solidity/interfaces/modules/resolution/IArbitratorModule.sol +++ b/solidity/interfaces/modules/resolution/IArbitratorModule.sol @@ -1,84 +1,106 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; -// import {IResolutionModule} from -// '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/resolution/IResolutionModule.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IResolutionModule} from + '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/resolution/IResolutionModule.sol'; -// /* -// * @title ArbitratorModule -// * @notice Module allowing an external arbitrator contract -// * to resolve a dispute. -// */ -// interface IArbitratorModule is IResolutionModule { -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ +/* + * @title ArbitratorModule + * @notice Module allowing an external arbitrator contract + * to resolve a dispute. + */ +interface IArbitratorModule is IResolutionModule { + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Thrown when an unauthorized caller calls a function only the arbitrator can call -// */ -// error ArbitratorModule_OnlyArbitrator(); + /** + * @notice Thrown when an unauthorized caller calls a function only the arbitrator can call + */ + error ArbitratorModule_OnlyArbitrator(); -// /** -// * @notice Thrown when trying to resolve a dispute that is not escalated -// */ -// error ArbitratorModule_InvalidDisputeId(); + /** + * @notice Thrown when trying to resolve a dispute that is not escalated + */ + error ArbitratorModule_InvalidDisputeId(); -// /** -// * @notice Thrown when the arbitrator address is the address zero -// */ -// error ArbitratorModule_InvalidArbitrator(); + /** + * @notice Thrown when the arbitrator address is the address zero + */ + error ArbitratorModule_InvalidArbitrator(); -// /** -// * @notice Thrown when the arbitrator returns an invalid resolution status -// */ -// error ArbitratorModule_InvalidResolutionStatus(); + /** + * @notice Thrown when the arbitrator returns an invalid resolution status + */ + error ArbitratorModule_InvalidResolutionStatus(); -// /*/////////////////////////////////////////////////////////////// -// ENUMS -// //////////////////////////////////////////////////////////////*/ + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + /** + * @notice Parameters of the request as stored in the module + * @param arbitrator The address of the arbitrator + */ + struct RequestParameters { + address arbitrator; + } -// /** -// * @notice Available status of the arbitration process -// */ -// enum ArbitrationStatus { -// Unknown, // The arbitration process has not started (default) -// Active, // The arbitration process is active -// Resolved // The arbitration process is resolved -// } + /*/////////////////////////////////////////////////////////////// + ENUMS + //////////////////////////////////////////////////////////////*/ -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ + /** + * @notice Available status of the arbitration process + */ + enum ArbitrationStatus { + Unknown, // The arbitration process has not started (default) + Active, // The arbitration process is active + Resolved // The arbitration process is resolved + } -// /** -// * @notice Returns the current arbitration status of a dispute -// * @param _disputeId The ID of the dispute -// * @return _disputeStatus The `ArbitrationStatus` of the dispute -// */ -// function getStatus(bytes32 _disputeId) external view returns (ArbitrationStatus _disputeStatus); + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Starts the arbitration process by calling `resolve` on the -// * arbitrator and flags the dispute as Active -// * @dev Only callable by the Oracle -// * @dev Will revert if the arbitrator address is the address zero -// * @param _disputeId The ID of the dispute -// */ -// function startResolution(bytes32 _disputeId) external; + /** + * @notice Returns the current arbitration status of a dispute + * @param _disputeId The ID of the dispute + * @return _disputeStatus The `ArbitrationStatus` of the dispute + */ + function getStatus(bytes32 _disputeId) external view returns (ArbitrationStatus _disputeStatus); -// /** -// * @notice Resolves the dispute by getting the answer from the arbitrator -// * and updating the dispute status -// * @dev Only callable by the Oracle -// * @param _disputeId The ID of the dispute -// */ -// function resolveDispute(bytes32 _disputeId) external; + /** + * @notice Starts the arbitration process by calling `resolve` on the + * arbitrator and flags the dispute as Active + * @dev Only callable by the Oracle + * @dev Will revert if the arbitrator address is the address zero + * @param _disputeId The ID of the dispute + */ + function startResolution( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; -// /** -// * @notice Returns the decoded data for a request -// * @param _requestId The ID of the request -// * @return _arbitrator The address of the arbitrator -// */ -// function decodeRequestData(bytes32 _requestId) external view returns (address _arbitrator); -// } + /** + * @notice Resolves the dispute by getting the answer from the arbitrator + * and updating the dispute status + * @dev Only callable by the Oracle + * @param _disputeId The ID of the dispute + */ + function resolveDispute( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; + + /** + * @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); +} diff --git a/solidity/test/unit/modules/resolution/ArbitratorModule.t.sol b/solidity/test/unit/modules/resolution/ArbitratorModule.t.sol index 7f13db93..66584655 100644 --- a/solidity/test/unit/modules/resolution/ArbitratorModule.t.sol +++ b/solidity/test/unit/modules/resolution/ArbitratorModule.t.sol @@ -1,328 +1,281 @@ -// // SPDX-License-Identifier: AGPL-3.0-only -// pragma solidity ^0.8.19; - -// import 'forge-std/Test.sol'; - -// import {Helpers} from '../../../utils/Helpers.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 { -// ArbitratorModule, -// IArbitratorModule, -// IArbitrator -// } from '../../../../contracts/modules/resolution/ArbitratorModule.sol'; - -// /** -// * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks -// */ -// contract ForTest_ArbitratorModule is ArbitratorModule { -// constructor(IOracle _oracle) ArbitratorModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } - -// function forTest_setDisputeStatus(bytes32 _disputeId, IArbitratorModule.ArbitrationStatus _status) public { -// _disputeData[_disputeId] = _status; -// } -// } - -// /** -// * @title Arbitrator Module Unit tests -// */ -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_ArbitratorModule public arbitratorModule; -// // A mock oracle -// IOracle public oracle; -// // A mock arbitrator -// IArbitrator public arbitrator; -// // Create a new dummy dispute -// IOracle.Dispute public mockDispute; - -// event ResolutionStarted(bytes32 indexed _requestId, bytes32 indexed _disputeId); -// event DisputeResolved(bytes32 indexed _requestId, bytes32 indexed _disputeId, IOracle.DisputeStatus _status); - -// /** -// * @notice Deploy the target and mock oracle -// */ -// function setUp() public { -// oracle = IOracle(makeAddr('Oracle')); -// vm.etch(address(oracle), hex'069420'); - -// arbitrator = IArbitrator(makeAddr('MockArbitrator')); -// vm.etch(address(arbitrator), hex'069420'); - -// arbitratorModule = new ForTest_ArbitratorModule(oracle); - -// mockDispute = IOracle.Dispute({ -// createdAt: block.timestamp, -// disputer: makeAddr('disputer'), -// proposer: makeAddr('proposer'), -// responseId: bytes32('69'), -// requestId: bytes32('69'), -// status: IOracle.DisputeStatus.Active -// }); -// } -// } - -// contract ArbitratorModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(arbitratorModule.moduleName(), 'ArbitratorModule'); -// } -// /** -// * @notice Test that the decodeRequestData function returns the correct values -// */ - -// function test_decodeRequestData(bytes32 _requestId, address _arbitrator) public { -// // Mock data -// bytes memory _requestData = abi.encode(address(_arbitrator)); - -// // Store the mock dispute -// arbitratorModule.forTest_setRequestData(_requestId, _requestData); - -// // Test: decode the given request data -// (address _arbitratorStored) = arbitratorModule.decodeRequestData(_requestId); - -// // Check: decoded values match original values? -// assertEq(_arbitratorStored, _arbitrator); -// } - -// /** -// * @notice Test that the status is correctly retrieved -// */ -// function test_getStatus(uint256 _status, bytes32 _disputeId) public { -// _status = bound(_status, 0, uint256(IArbitratorModule.ArbitrationStatus.Resolved)); -// IArbitratorModule.ArbitrationStatus _arbitratorStatus = IArbitratorModule.ArbitrationStatus(_status); - -// // Store the mock dispute -// arbitratorModule.forTest_setDisputeStatus(_disputeId, _arbitratorStatus); - -// // Check: The correct status is returned? -// assertEq(uint256(arbitratorModule.getStatus(_disputeId)), uint256(_status)); -// } -// } - -// contract ArbitratorModule_Unit_StartResolution is BaseTest { -// /** -// * @notice Test that the escalate function works as expected -// */ -// function test_startResolution(bytes32 _disputeId, bytes32 _requestId) public { -// // Mock and expect the dummy dispute -// mockDispute.requestId = _requestId; -// _mockAndExpect(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(mockDispute)); - -// // Store the requestData -// bytes memory _requestData = abi.encode(address(arbitrator)); -// arbitratorModule.forTest_setRequestData(_requestId, _requestData); - -// // Mock and expect the callback to the arbitrator -// _mockAndExpect(address(arbitrator), abi.encodeCall(arbitrator.resolve, (_disputeId)), abi.encode(bytes(''))); - -// vm.prank(address(oracle)); -// arbitratorModule.startResolution(_disputeId); - -// // Check: is status now Escalated? -// assertEq(uint256(arbitratorModule.getStatus(_disputeId)), uint256(IArbitratorModule.ArbitrationStatus.Active)); -// } - -// function test_emitsEvent(bytes32 _disputeId, bytes32 _requestId) public { -// // Mock and expect the dummy dispute -// mockDispute.requestId = _requestId; -// _mockAndExpect(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(mockDispute)); - -// // Store the requestData -// bytes memory _requestData = abi.encode(address(arbitrator)); -// arbitratorModule.forTest_setRequestData(_requestId, _requestData); - -// // Mock and expect the callback to the arbitrator -// _mockAndExpect(address(arbitrator), abi.encodeCall(arbitrator.resolve, (_disputeId)), abi.encode(bytes(''))); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(arbitratorModule)); -// emit ResolutionStarted(_requestId, _disputeId); - -// vm.prank(address(oracle)); -// arbitratorModule.startResolution(_disputeId); -// } - -// function test_revertInvalidCaller(address _caller, bytes32 _disputeId) public { -// vm.assume(_caller != address(oracle)); - -// // Check: does it revert if the caller is not the Oracle? -// vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); - -// vm.prank(_caller); -// arbitratorModule.startResolution(_disputeId); -// } - -// function test_revertIfEmptyArbitrator(bytes32 _disputeId, bytes32 _requestId) public { -// // Mock and expect the dummy dispute -// mockDispute.requestId = _requestId; -// _mockAndExpect(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(mockDispute)); - -// // Store the requestData -// bytes memory _requestData = abi.encode(address(0)); -// arbitratorModule.forTest_setRequestData(_requestId, _requestData); - -// // Check: revert? -// vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidArbitrator.selector)); - -// // Test: escalate the dispute -// vm.prank(address(oracle)); -// arbitratorModule.startResolution(_disputeId); -// } -// } - -// contract ArbitratorModule_Unit_ResolveDispute is BaseTest { -// /** -// * @notice Test that the resolve function works as expected -// */ -// function test_resolveDispute(bytes32 _disputeId, bytes32 _requestId, uint256 _status) public { -// vm.assume(_status <= uint256(IOracle.DisputeStatus.Lost)); -// vm.assume(_status > uint256(IOracle.DisputeStatus.Escalated)); -// IOracle.DisputeStatus _arbitratorStatus = IOracle.DisputeStatus(_status); - -// // Store the mock dispute -// bytes memory _requestData = abi.encode(address(arbitrator)); -// arbitratorModule.forTest_setRequestData(_requestId, _requestData); - -// // Mock the dummy dispute -// mockDispute.requestId = _requestId; -// mockDispute.status = IOracle.DisputeStatus.Escalated; - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(mockDispute)); - -// // Mock and expect getAnswer to be called on the arbitrator -// _mockAndExpect( -// address(arbitrator), abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus) -// ); - -// // Mock and expect IOracle.updateDisputeStatus to be called -// _mockAndExpect( -// address(oracle), abi.encodeCall(oracle.updateDisputeStatus, (_disputeId, _arbitratorStatus)), abi.encode() -// ); - -// vm.prank(address(oracle)); -// arbitratorModule.resolveDispute(_disputeId); - -// // Check: is status now Resolved? -// assertEq(uint256(arbitratorModule.getStatus(_disputeId)), uint256(IArbitratorModule.ArbitrationStatus.Resolved)); -// } - -// function test_revertsIfInvalidResolveStatus(bytes32 _disputeId, bytes32 _requestId, uint256 _status) public { -// vm.assume(_status <= uint256(IOracle.DisputeStatus.Escalated)); -// IOracle.DisputeStatus _arbitratorStatus = IOracle.DisputeStatus(_status); - -// // Store the mock dispute -// bytes memory _requestData = abi.encode(address(arbitrator)); -// arbitratorModule.forTest_setRequestData(_requestId, _requestData); - -// // Mock the dummy dispute -// mockDispute.requestId = _requestId; -// mockDispute.status = IOracle.DisputeStatus.Escalated; - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(mockDispute)); - -// // Mock and expect getAnswer to be called on the arbitrator -// _mockAndExpect( -// address(arbitrator), abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus) -// ); - -// // Check: does it revert if the resolution status is invalid? -// vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidResolutionStatus.selector)); - -// vm.prank(address(oracle)); -// arbitratorModule.resolveDispute(_disputeId); -// } - -// function test_emitsEvent(bytes32 _disputeId, bytes32 _requestId, uint256 _status) public { -// vm.assume(_status <= uint256(IOracle.DisputeStatus.Lost)); -// vm.assume(_status > uint256(IOracle.DisputeStatus.Escalated)); -// IOracle.DisputeStatus _arbitratorStatus = IOracle.DisputeStatus(_status); - -// // Store the mock dispute -// bytes memory _requestData = abi.encode(address(arbitrator)); -// arbitratorModule.forTest_setRequestData(_requestId, _requestData); - -// // Mock the dummy dispute -// mockDispute.requestId = _requestId; -// mockDispute.status = IOracle.DisputeStatus.Escalated; - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(mockDispute)); - -// // Mock and expect getAnswer to be called on the arbitrator -// _mockAndExpect( -// address(arbitrator), abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus) -// ); - -// // Mock and expect IOracle.updateDisputeStatus to be called -// _mockAndExpect( -// address(oracle), abi.encodeCall(oracle.updateDisputeStatus, (_disputeId, _arbitratorStatus)), abi.encode() -// ); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(arbitratorModule)); -// emit DisputeResolved(_requestId, _disputeId, _arbitratorStatus); - -// vm.prank(address(oracle)); -// arbitratorModule.resolveDispute(_disputeId); -// } - -// /** -// * @notice resolve dispute reverts if the dispute status isn't Active -// */ -// function test_revertIfInvalidDispute(bytes32 _requestId, bytes32 _responseId, bytes32 _disputeId) public { -// // Store the requestData -// bytes memory _requestData = abi.encode(address(arbitrator)); -// arbitratorModule.forTest_setRequestData(_requestId, _requestData); - -// // Test the 3 different invalid status (None, Won, Lost) -// for (uint256 _status; _status < uint256(type(IOracle.DisputeStatus).max); _status++) { -// if (IOracle.DisputeStatus(_status) == IOracle.DisputeStatus.Escalated) continue; -// // Create a new dummy dispute -// IOracle.Dispute memory _dispute = IOracle.Dispute({ -// createdAt: block.timestamp, -// disputer: makeAddr('disputer'), -// proposer: makeAddr('proposer'), -// responseId: _responseId, -// requestId: _requestId, -// status: IOracle.DisputeStatus(_status) -// }); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(_dispute)); - -// // Check: does it revert if the dispute id is invalid? -// vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidDisputeId.selector)); - -// vm.prank(address(oracle)); -// arbitratorModule.resolveDispute(_disputeId); -// } -// } - -// /** -// * @notice Test that the resolve function reverts if the caller isn't the arbitrator -// */ -// function test_revertIfWrongSender(bytes32 _disputeId, bytes32 _requestId, address _caller) public { -// vm.assume(_caller != address(oracle)); - -// // Store the mock dispute -// bytes memory _requestData = abi.encode(address(arbitrator)); -// arbitratorModule.forTest_setRequestData(_requestId, _requestData); - -// // Check: does it revert if not called by the Oracle? -// vm.expectRevert(IModule.Module_OnlyOracle.selector); - -// vm.prank(_caller); -// arbitratorModule.resolveDispute(_disputeId); -// } -// } +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; + +import {Helpers} from '../../../utils/Helpers.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 { + ArbitratorModule, + IArbitratorModule, + IArbitrator +} from '../../../../contracts/modules/resolution/ArbitratorModule.sol'; + +/** + * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks + */ +contract ForTest_ArbitratorModule is ArbitratorModule { + constructor(IOracle _oracle) ArbitratorModule(_oracle) {} + + function forTest_setDisputeStatus(bytes32 _disputeId, IArbitratorModule.ArbitrationStatus _status) public { + _disputeData[_disputeId] = _status; + } +} + +/** + * @title Arbitrator Module Unit tests + */ +contract BaseTest is Test, Helpers { + // The target contract + ForTest_ArbitratorModule public arbitratorModule; + // A mock oracle + IOracle public oracle; + // A mock arbitrator + IArbitrator public arbitrator; + // Create a new dummy dispute + IOracle.Dispute public mockDispute; + // Create a new dummy response + IOracle.Response public mockResponse; + address internal _proposer = makeAddr('proposer'); + bytes32 public mockId = bytes32('69'); + + event ResolutionStarted(bytes32 indexed _requestId, bytes32 indexed _disputeId); + event DisputeResolved(bytes32 indexed _requestId, bytes32 indexed _disputeId, IOracle.DisputeStatus _status); + + /** + * @notice Deploy the target and mock oracle + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + arbitrator = IArbitrator(makeAddr('MockArbitrator')); + vm.etch(address(arbitrator), hex'069420'); + + arbitratorModule = new ForTest_ArbitratorModule(oracle); + + mockDispute = IOracle.Dispute({ + disputer: makeAddr('disputer'), + proposer: makeAddr('proposer'), + responseId: bytes32('69'), + requestId: bytes32('69') + }); + + mockResponse = IOracle.Response({proposer: _proposer, requestId: mockId, response: bytes('')}); + } +} + +contract ArbitratorModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(arbitratorModule.moduleName(), 'ArbitratorModule'); + } + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + + function test_decodeRequestData(address _arbitrator) public { + // Mock data + bytes memory _requestData = abi.encode(address(_arbitrator)); + + // Test: decode the given request data + IArbitratorModule.RequestParameters memory _requestParameters = arbitratorModule.decodeRequestData(_requestData); + + // Check: decoded values match original values? + assertEq(_requestParameters.arbitrator, _arbitrator); + } + + /** + * @notice Test that the status is correctly retrieved + */ + function test_getStatus(uint256 _status, bytes32 _disputeId) public { + _status = bound(_status, 0, uint256(IArbitratorModule.ArbitrationStatus.Resolved)); + IArbitratorModule.ArbitrationStatus _arbitratorStatus = IArbitratorModule.ArbitrationStatus(_status); + + // Store the mock dispute + arbitratorModule.forTest_setDisputeStatus(_disputeId, _arbitratorStatus); + + // Check: The correct status is returned? + assertEq(uint256(arbitratorModule.getStatus(_disputeId)), uint256(_status)); + } +} + +contract ArbitratorModule_Unit_StartResolution is BaseTest { + /** + * @notice Test that the escalate function works as expected + */ + function test_startResolution(bytes32 _disputeId, IOracle.Request calldata _request) public { + // Mock and expect the dummy dispute + mockDispute.requestId = _getId(_request); + + // Store the requestData + bytes memory _requestData = abi.encode(address(arbitrator)); + + // Mock and expect the callback to the arbitrator + _mockAndExpect(address(arbitrator), abi.encodeCall(arbitrator.resolve, (_disputeId)), abi.encode(bytes(''))); + + vm.prank(address(oracle)); + arbitratorModule.startResolution(_disputeId, _request, mockResponse, mockDispute); + + // Check: is status now Escalated? + assertEq(uint256(arbitratorModule.getStatus(_disputeId)), uint256(IArbitratorModule.ArbitrationStatus.Active)); + } + + function test_emitsEvent(bytes32 _disputeId, IOracle.Request calldata _request) public { + // Mock and expect the dummy dispute + mockDispute.requestId = _getId(_request); + + // Store the requestData + bytes memory _requestData = abi.encode(address(arbitrator)); + + // Mock and expect the callback to the arbitrator + _mockAndExpect(address(arbitrator), abi.encodeCall(arbitrator.resolve, (_disputeId)), abi.encode(bytes(''))); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(arbitratorModule)); + emit ResolutionStarted(mockDispute.requestId, _disputeId); + + vm.prank(address(oracle)); + arbitratorModule.startResolution(_disputeId, _request, mockResponse, mockDispute); + } + + function test_revertInvalidCaller(address _caller, bytes32 _disputeId, IOracle.Request calldata _request) public { + vm.assume(_caller != address(oracle)); + + // Check: does it revert if the caller is not the Oracle? + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + + vm.prank(_caller); + arbitratorModule.startResolution(_disputeId, _request, mockResponse, mockDispute); + } + + function test_revertIfEmptyArbitrator(bytes32 _disputeId, IOracle.Request calldata _request) public { + // Mock and expect the dummy dispute + mockDispute.requestId = _getId(_request); + + // Store the requestData + bytes memory _requestData = abi.encode(address(0)); + + // Check: revert? + vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidArbitrator.selector)); + + // Test: escalate the dispute + vm.prank(address(oracle)); + arbitratorModule.startResolution(_disputeId, _request, mockResponse, mockDispute); + } +} + +contract ArbitratorModule_Unit_ResolveDispute is BaseTest { + /** + * @notice Test that the resolve function works as expected + */ + function test_resolveDispute(bytes32 _disputeId, uint256 _status, IOracle.Request calldata _request) public { + vm.assume(_status <= uint256(IOracle.DisputeStatus.Lost)); + vm.assume(_status > uint256(IOracle.DisputeStatus.Escalated)); + IOracle.DisputeStatus _arbitratorStatus = IOracle.DisputeStatus(_status); + + // Mock and expect getAnswer to be called on the arbitrator + _mockAndExpect( + address(arbitrator), abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus) + ); + + // Mock and expect IOracle.updateDisputeStatus to be called + _mockAndExpect( + address(oracle), + abi.encodeCall(oracle.updateDisputeStatus, (_request, mockResponse, mockDispute, _arbitratorStatus)), + abi.encode() + ); + + vm.prank(address(oracle)); + arbitratorModule.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + + // Check: is status now Resolved? + assertEq(uint256(arbitratorModule.getStatus(_disputeId)), uint256(IArbitratorModule.ArbitrationStatus.Resolved)); + } + + function test_revertsIfInvalidResolveStatus( + bytes32 _disputeId, + uint256 _status, + IOracle.Request calldata _request + ) public { + vm.assume(_status <= uint256(IOracle.DisputeStatus.Escalated)); + IOracle.DisputeStatus _arbitratorStatus = IOracle.DisputeStatus(_status); + + // Mock and expect getAnswer to be called on the arbitrator + _mockAndExpect( + address(arbitrator), abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus) + ); + + // Check: does it revert if the resolution status is invalid? + vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidResolutionStatus.selector)); + + vm.prank(address(oracle)); + arbitratorModule.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + } + + function test_emitsEvent(bytes32 _disputeId, uint256 _status, IOracle.Request calldata _request) public { + vm.assume(_status <= uint256(IOracle.DisputeStatus.Lost)); + vm.assume(_status > uint256(IOracle.DisputeStatus.Escalated)); + IOracle.DisputeStatus _arbitratorStatus = IOracle.DisputeStatus(_status); + + // Mock and expect getAnswer to be called on the arbitrator + _mockAndExpect( + address(arbitrator), abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus) + ); + + // Mock and expect IOracle.updateDisputeStatus to be called + _mockAndExpect( + address(oracle), + abi.encodeCall(oracle.updateDisputeStatus, (_request, mockResponse, mockDispute, _arbitratorStatus)), + abi.encode() + ); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(arbitratorModule)); + emit DisputeResolved(_getId(_request), _disputeId, _arbitratorStatus); + + vm.prank(address(oracle)); + arbitratorModule.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + } + + /** + * @notice resolve dispute reverts if the dispute status isn't Active + */ + function test_revertIfInvalidDispute(IOracle.Request calldata _request, bytes32 _disputeId) public { + // Test the 3 different invalid status (None, Won, Lost) + for (uint256 _status; _status < uint256(type(IOracle.DisputeStatus).max); _status++) { + if (IOracle.DisputeStatus(_status) == IOracle.DisputeStatus.Escalated) continue; + // Create a new dummy dispute + IOracle.Dispute memory _dispute = IOracle.Dispute({ + disputer: makeAddr('disputer'), + proposer: makeAddr('proposer'), + responseId: _getId(mockResponse), + requestId: _getId(_request) + }); + + // Check: does it revert if the dispute id is invalid? + vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidDisputeId.selector)); + + vm.prank(address(oracle)); + arbitratorModule.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + } + } + + /** + * @notice Test that the resolve function reverts if the caller isn't the arbitrator + */ + function test_revertIfWrongSender(bytes32 _disputeId, address _caller, IOracle.Request calldata _request) public { + vm.assume(_caller != address(oracle)); + + // Check: does it revert if not called by the Oracle? + vm.expectRevert(IModule.Module_OnlyOracle.selector); + + vm.prank(_caller); + arbitratorModule.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + } +} From ec62b96cbf9995d75d5ed4880b62ae540d113715 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:34:09 +0400 Subject: [PATCH 13/53] feat: update `prophet-core` package --- package.json | 2 +- yarn.lock | 72 +++++++++++++++++++++++++--------------------------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index b7700bc1..e85440a1 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "package.json": "sort-package-json" }, "dependencies": { - "@defi-wonderland/prophet-core-contracts": "0.0.0-d05a00d0", + "@defi-wonderland/prophet-core-contracts": "0.0.0-48b0248d", "@defi-wonderland/solidity-utils": "0.0.0-3e9c8e8b", "@openzeppelin/contracts": "^4.9.3", "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", diff --git a/yarn.lock b/yarn.lock index c61b7709..23515a4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -192,10 +192,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@defi-wonderland/prophet-core-contracts@0.0.0-d05a00d0": - version "0.0.0-d05a00d0" - resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-d05a00d0.tgz#1357d917fe46a5a12faa67f557e990255dda14fd" - integrity sha512-F/y0r/qDLFACzsN7Y2VRAPIS9Yhx2btU/m7cQT7T84TbIxAmBGVw6/7nb+HeIbXh+QDO90RP6vHAdQOow/q1Xw== +"@defi-wonderland/prophet-core-contracts@0.0.0-48b0248d": + version "0.0.0-48b0248d" + resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-48b0248d.tgz#16d0473360074f17b66199c8e3660b71a3d72ad4" + integrity sha512-bEufdaPkLcg1VuYpTWRB5Xf4pmpV3wi0487taGI4A+YtwhMsIh9ZCNPdgWssqLvIfMPlB4FdGOM936l1yqAKYQ== dependencies: "@defi-wonderland/solidity-utils" "0.0.0-3e9c8e8b" "@openzeppelin/contracts" "^4.9.3" @@ -324,9 +324,9 @@ ts-essentials "^7.0.1" "@types/minimist@^1.2.0": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.4.tgz#81f886786411c45bba3f33e781ab48bd56bfca2e" - integrity sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@20.5.1": version "20.5.1" @@ -334,9 +334,9 @@ integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== "@types/normalize-package-data@^2.4.0": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz#291c243e4b94dbfbc0c0ee26b7666f1d5c030e2c" - integrity sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/prettier@^2.1.1": version "2.7.3" @@ -357,9 +357,9 @@ acorn-jsx@^5.0.0: integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== acorn@^6.0.7: version "6.4.2" @@ -367,9 +367,9 @@ acorn@^6.0.7: integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^8.4.1: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== add-stream@^1.0.0: version "1.0.0" @@ -1181,14 +1181,10 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -"ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": - version "1.0.0" - resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" - -"ds-test@https://github.com/dapphub/ds-test": +"ds-test@git+https://github.com/dapphub/ds-test.git", "ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" uid e282159d5170298eb2455a6c05280ab5a73a4ef0 - resolved "https://github.com/dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0" + resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" "ds-test@https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" @@ -1457,9 +1453,9 @@ fast-diff@^1.1.2, fast-diff@^1.2.0: integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== fast-glob@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -1570,14 +1566,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +"forge-std@git+https://github.com/foundry-rs/forge-std.git": + version "1.7.1" + resolved "git+https://github.com/foundry-rs/forge-std.git#37a37ab73364d6644bfe11edf88a07880f99bd56" + "forge-std@git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e": version "1.5.6" resolved "git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e" -"forge-std@https://github.com/foundry-rs/forge-std": - version "1.7.1" - resolved "https://github.com/foundry-rs/forge-std#267acd30a625086b3f16e1a28cfe0c5097fa46b8" - "forge-std@https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421": version "1.7.1" resolved "https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421" @@ -2858,9 +2854,9 @@ progress@^2.0.0: integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== q@^1.5.1: version "1.5.1" @@ -3787,9 +3783,9 @@ universalify@^0.1.0: integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== uri-js@^4.2.2: version "4.4.1" @@ -3916,9 +3912,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^2.2.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" - integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" From 3caf37c5d2aca4cedb4b4fddc80437c8b55a0e94 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:34:39 +0400 Subject: [PATCH 14/53] perf: optimize `ERC20ResolutionModule` --- .../resolution/erc20_resolution_module.md | 2 +- .../resolution/ERC20ResolutionModule.sol | 237 ++--- .../resolution/IERC20ResolutionModule.sol | 297 ++++--- .../resolution/ERC20ResolutionModule.t.sol | 809 +++++++++--------- 4 files changed, 681 insertions(+), 664 deletions(-) diff --git a/docs/src/content/modules/resolution/erc20_resolution_module.md b/docs/src/content/modules/resolution/erc20_resolution_module.md index e8073699..3905c8fb 100644 --- a/docs/src/content/modules/resolution/erc20_resolution_module.md +++ b/docs/src/content/modules/resolution/erc20_resolution_module.md @@ -10,7 +10,7 @@ The `ERC20ResolutionModule` is a dispute resolution module that decides on the o ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: Decodes the request data associated with a given request ID. +- `decodeRequestData(bytes calldata _data)`: Decodes the request data associated with a given request ID. - `startResolution(bytes32 _disputeId)`: Starts the resolution process for a given dispute. - `castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes)`: Allows a user to cast votes for a dispute. - `resolveDispute(bytes32 _disputeId)`: Resolves a dispute based on the votes cast. diff --git a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol index 78b0ce2c..dfd9197b 100644 --- a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol +++ b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol @@ -1,112 +1,125 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// // solhint-disable-next-line no-unused-import -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; -// import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.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 {IERC20ResolutionModule} from '../../../interfaces/modules/resolution/IERC20ResolutionModule.sol'; - -// contract ERC20ResolutionModule is Module, IERC20ResolutionModule { -// using SafeERC20 for IERC20; -// using EnumerableSet for EnumerableSet.AddressSet; - -// /// @inheritdoc IERC20ResolutionModule -// mapping(bytes32 _disputeId => Escalation _escalation) public escalations; - -// /// @inheritdoc IERC20ResolutionModule -// mapping(bytes32 _disputeId => mapping(address _voter => uint256 _numOfVotes)) public votes; - -// mapping(bytes32 _disputeId => EnumerableSet.AddressSet _votersSet) private _voters; - -// constructor(IOracle _oracle) Module(_oracle) {} - -// /// @inheritdoc IModule -// function moduleName() external pure returns (string memory _moduleName) { -// return 'ERC20ResolutionModule'; -// } - -// /// @inheritdoc IERC20ResolutionModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } - -// /// @inheritdoc IERC20ResolutionModule -// function startResolution(bytes32 _disputeId) external onlyOracle { -// escalations[_disputeId].startTime = block.timestamp; -// emit VotingPhaseStarted(block.timestamp, _disputeId); -// } - -// /// @inheritdoc IERC20ResolutionModule -// function castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes) public { -// IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId); -// if (_dispute.createdAt == 0) revert ERC20ResolutionModule_NonExistentDispute(); -// if (_dispute.status != IOracle.DisputeStatus.None) revert ERC20ResolutionModule_AlreadyResolved(); - -// Escalation memory _escalation = escalations[_disputeId]; -// if (_escalation.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated(); - -// RequestParameters memory _params = decodeRequestData(_requestId); -// uint256 _deadline = _escalation.startTime + _params.timeUntilDeadline; -// if (block.timestamp >= _deadline) revert ERC20ResolutionModule_VotingPhaseOver(); - -// votes[_disputeId][msg.sender] += _numberOfVotes; - -// _voters[_disputeId].add(msg.sender); -// escalations[_disputeId].totalVotes += _numberOfVotes; - -// _params.votingToken.safeTransferFrom(msg.sender, address(this), _numberOfVotes); -// emit VoteCast(msg.sender, _disputeId, _numberOfVotes); -// } - -// /// @inheritdoc IERC20ResolutionModule -// function resolveDispute(bytes32 _disputeId) external onlyOracle { -// // 0. Check disputeId actually exists and that it isn't resolved already -// IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId); -// if (_dispute.createdAt == 0) revert ERC20ResolutionModule_NonExistentDispute(); -// if (_dispute.status != IOracle.DisputeStatus.None) revert ERC20ResolutionModule_AlreadyResolved(); - -// Escalation memory _escalation = escalations[_disputeId]; -// // 1. Check that the dispute is actually escalated -// if (_escalation.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated(); - -// // 2. Check that voting deadline is over -// RequestParameters memory _params = decodeRequestData(_dispute.requestId); -// uint256 _deadline = _escalation.startTime + _params.timeUntilDeadline; -// if (block.timestamp < _deadline) revert ERC20ResolutionModule_OnGoingVotingPhase(); - -// uint256 _quorumReached = _escalation.totalVotes >= _params.minVotesForQuorum ? 1 : 0; - -// address[] memory __voters = _voters[_disputeId].values(); - -// // 5. Update status -// if (_quorumReached == 1) { -// ORACLE.updateDisputeStatus(_disputeId, IOracle.DisputeStatus.Won); -// emit DisputeResolved(_dispute.requestId, _disputeId, IOracle.DisputeStatus.Won); -// } else { -// ORACLE.updateDisputeStatus(_disputeId, IOracle.DisputeStatus.Lost); -// emit DisputeResolved(_dispute.requestId, _disputeId, IOracle.DisputeStatus.Lost); -// } - -// uint256 _votersLength = __voters.length; - -// // 6. Return tokens -// for (uint256 _i; _i < _votersLength;) { -// address _voter = __voters[_i]; -// _params.votingToken.safeTransfer(_voter, votes[_disputeId][_voter]); -// unchecked { -// ++_i; -// } -// } -// } - -// /// @inheritdoc IERC20ResolutionModule -// function getVoters(bytes32 _disputeId) external view returns (address[] memory __voters) { -// __voters = _voters[_disputeId].values(); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// solhint-disable-next-line no-unused-import +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.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 {IERC20ResolutionModule} from '../../../interfaces/modules/resolution/IERC20ResolutionModule.sol'; + +contract ERC20ResolutionModule is Module, IERC20ResolutionModule { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + + /// @inheritdoc IERC20ResolutionModule + mapping(bytes32 _disputeId => Escalation _escalation) public escalations; + + /// @inheritdoc IERC20ResolutionModule + mapping(bytes32 _disputeId => mapping(address _voter => uint256 _numOfVotes)) public votes; + + mapping(bytes32 _disputeId => EnumerableSet.AddressSet _votersSet) private _voters; + + constructor(IOracle _oracle) Module(_oracle) {} + + /// @inheritdoc IModule + function moduleName() external pure returns (string memory _moduleName) { + return 'ERC20ResolutionModule'; + } + + /// @inheritdoc IERC20ResolutionModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } + + /// @inheritdoc IERC20ResolutionModule + function startResolution( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + escalations[_disputeId].startTime = block.timestamp; + emit VotingPhaseStarted(block.timestamp, _disputeId); + } + + /// @inheritdoc IERC20ResolutionModule + function castVote( + IOracle.Request calldata _request, + IOracle.Dispute calldata _dispute, + uint256 _numberOfVotes + ) public { + bytes32 _disputeId = _getId(_dispute); + if (ORACLE.createdAt(_disputeId) == 0) revert ERC20ResolutionModule_NonExistentDispute(); + if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.None) revert ERC20ResolutionModule_AlreadyResolved(); + + Escalation memory _escalation = escalations[_disputeId]; + if (_escalation.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated(); + + RequestParameters memory _params = decodeRequestData(_request.resolutionModuleData); + uint256 _deadline = _escalation.startTime + _params.timeUntilDeadline; + if (block.timestamp >= _deadline) revert ERC20ResolutionModule_VotingPhaseOver(); + + votes[_disputeId][msg.sender] += _numberOfVotes; + + _voters[_disputeId].add(msg.sender); + escalations[_disputeId].totalVotes += _numberOfVotes; + + _params.votingToken.safeTransferFrom(msg.sender, address(this), _numberOfVotes); + emit VoteCast(msg.sender, _disputeId, _numberOfVotes); + } + + /// @inheritdoc IERC20ResolutionModule + function resolveDispute( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + // 0. Check disputeId actually exists and that it isn't resolved already + if (ORACLE.createdAt(_disputeId) == 0) revert ERC20ResolutionModule_NonExistentDispute(); + if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.None) revert ERC20ResolutionModule_AlreadyResolved(); + + Escalation memory _escalation = escalations[_disputeId]; + // 1. Check that the dispute is actually escalated + if (_escalation.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated(); + + // 2. Check that voting deadline is over + RequestParameters memory _params = decodeRequestData(_request.resolutionModuleData); + uint256 _deadline = _escalation.startTime + _params.timeUntilDeadline; + if (block.timestamp < _deadline) revert ERC20ResolutionModule_OnGoingVotingPhase(); + + uint256 _quorumReached = _escalation.totalVotes >= _params.minVotesForQuorum ? 1 : 0; + + address[] memory __voters = _voters[_disputeId].values(); + + // 5. Update status + if (_quorumReached == 1) { + ORACLE.updateDisputeStatus(_request, _response, _dispute, IOracle.DisputeStatus.Won); + emit DisputeResolved(_dispute.requestId, _disputeId, IOracle.DisputeStatus.Won); + } else { + ORACLE.updateDisputeStatus(_request, _response, _dispute, IOracle.DisputeStatus.Lost); + emit DisputeResolved(_dispute.requestId, _disputeId, IOracle.DisputeStatus.Lost); + } + + uint256 _votersLength = __voters.length; + + // 6. Return tokens + for (uint256 _i; _i < _votersLength;) { + address _voter = __voters[_i]; + _params.votingToken.safeTransfer(_voter, votes[_disputeId][_voter]); + unchecked { + ++_i; + } + } + } + + /// @inheritdoc IERC20ResolutionModule + function getVoters(bytes32 _disputeId) external view returns (address[] memory __voters) { + __voters = _voters[_disputeId].values(); + } +} diff --git a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol index 6e4bc7ad..58cb623b 100644 --- a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol +++ b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol @@ -1,142 +1,155 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IResolutionModule} from -// '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/resolution/IResolutionModule.sol'; -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; - -// /** -// * @title ERC20ResolutionModule -// * @notice This contract allows for disputes to be resolved by a voting process. -// * The voting process is started by the oracle and -// * the voting phase lasts for a certain amount of time. During this time, anyone can vote on the dispute. Once the voting -// * phase is over, the votes are tallied and if the votes in favor of the dispute are greater than the votes against the -// * dispute, the dispute is resolved in favor of the dispute. Otherwise, the dispute is resolved against the dispute. -// */ -// interface IERC20ResolutionModule is IResolutionModule { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Emitted when a voter casts their vote on a dispute -// * @param _voter The address of the voter -// * @param _disputeId The id of the dispute -// * @param _numberOfVotes The number of votes cast by the voter -// */ -// event VoteCast(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); - -// /** -// * @notice Emitted when the voting phase has started -// * @param _startTime The time when the voting phase started -// * @param _disputeId The ID of the dispute -// */ -// event VotingPhaseStarted(uint256 _startTime, bytes32 _disputeId); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Throws if the caller is not the dispute module -// */ -// error ERC20ResolutionModule_OnlyDisputeModule(); - -// /** -// * @notice Throws if the dispute has not been escalated -// */ -// error ERC20ResolutionModule_DisputeNotEscalated(); - -// /** -// * @notice Throws if the dispute is unresolved -// */ -// error ERC20ResolutionModule_UnresolvedDispute(); - -// /** -// * @notice Throws if the voting phase is over -// */ -// error ERC20ResolutionModule_VotingPhaseOver(); - -// /** -// * @notice Throws if the voting phase is ongoing -// */ -// error ERC20ResolutionModule_OnGoingVotingPhase(); - -// /** -// * @notice Throws if the dispute does not exist -// */ -// error ERC20ResolutionModule_NonExistentDispute(); - -// /** -// * @notice Throws if the dispute has already been resolved -// */ -// error ERC20ResolutionModule_AlreadyResolved(); - -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Parameters of the request as stored in the module -// * @param votingToken The token used to vote -// * @param minVotesForQuorum The minimum amount of votes to win the dispute -// * @param timeUntilDeadline The time until the voting phase ends -// */ -// struct RequestParameters { -// IERC20 votingToken; -// uint256 minVotesForQuorum; -// uint256 timeUntilDeadline; -// } - -// /** -// * @notice Escalation data for a dispute -// * @param startTime The timestamp at which the dispute was escalated -// * @param totalVotes The total amount of votes cast for the dispute -// */ -// struct Escalation { -// uint256 startTime; -// uint256 totalVotes; -// } - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Returns the escalation data for a dispute -// * @param _disputeId The id of the dispute -// * @return _startTime The timestamp at which the dispute was escalated -// * @return _totalVotes The total amount of votes cast for the dispute -// */ -// function escalations(bytes32 _disputeId) external view returns (uint256 _startTime, uint256 _totalVotes); - -// function votes(bytes32 _disputeId, address _voter) external view returns (uint256 _votes); - -// /** -// * @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); - -// /// @inheritdoc IResolutionModule -// function startResolution(bytes32 _disputeId) external; - -// /** -// * @notice Casts a vote in favor of a dispute -// * @param _requestId The id of the request being disputed -// * @param _disputeId The id of the dispute being voted on -// * @param _numberOfVotes The number of votes to cast -// */ -// function castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes) external; - -// /// @inheritdoc IResolutionModule -// function resolveDispute(bytes32 _disputeId) external; - -// /** -// * @notice Gets the voters of a dispute -// * @param _disputeId The id of the dispute -// * @return _voters The addresses of the voters -// */ -// function getVoters(bytes32 _disputeId) external view returns (address[] memory _voters); -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IResolutionModule} from + '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/resolution/IResolutionModule.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +/** + * @title ERC20ResolutionModule + * @notice This contract allows for disputes to be resolved by a voting process. + * The voting process is started by the oracle and + * the voting phase lasts for a certain amount of time. During this time, anyone can vote on the dispute. Once the voting + * phase is over, the votes are tallied and if the votes in favor of the dispute are greater than the votes against the + * dispute, the dispute is resolved in favor of the dispute. Otherwise, the dispute is resolved against the dispute. + */ +interface IERC20ResolutionModule is IResolutionModule { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Emitted when a voter casts their vote on a dispute + * @param _voter The address of the voter + * @param _disputeId The id of the dispute + * @param _numberOfVotes The number of votes cast by the voter + */ + event VoteCast(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); + + /** + * @notice Emitted when the voting phase has started + * @param _startTime The time when the voting phase started + * @param _disputeId The ID of the dispute + */ + event VotingPhaseStarted(uint256 _startTime, bytes32 _disputeId); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Throws if the caller is not the dispute module + */ + error ERC20ResolutionModule_OnlyDisputeModule(); + + /** + * @notice Throws if the dispute has not been escalated + */ + error ERC20ResolutionModule_DisputeNotEscalated(); + + /** + * @notice Throws if the dispute is unresolved + */ + error ERC20ResolutionModule_UnresolvedDispute(); + + /** + * @notice Throws if the voting phase is over + */ + error ERC20ResolutionModule_VotingPhaseOver(); + + /** + * @notice Throws if the voting phase is ongoing + */ + error ERC20ResolutionModule_OnGoingVotingPhase(); + + /** + * @notice Throws if the dispute does not exist + */ + error ERC20ResolutionModule_NonExistentDispute(); + + /** + * @notice Throws if the dispute has already been resolved + */ + error ERC20ResolutionModule_AlreadyResolved(); + + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Parameters of the request as stored in the module + * @param votingToken The token used to vote + * @param minVotesForQuorum The minimum amount of votes to win the dispute + * @param timeUntilDeadline The time until the voting phase ends + */ + struct RequestParameters { + IERC20 votingToken; + uint256 minVotesForQuorum; + uint256 timeUntilDeadline; + } + + /** + * @notice Escalation data for a dispute + * @param startTime The timestamp at which the dispute was escalated + * @param totalVotes The total amount of votes cast for the dispute + */ + struct Escalation { + uint256 startTime; + uint256 totalVotes; + } + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the escalation data for a dispute + * @param _disputeId The id of the dispute + * @return _startTime The timestamp at which the dispute was escalated + * @return _totalVotes The total amount of votes cast for the dispute + */ + function escalations(bytes32 _disputeId) external view returns (uint256 _startTime, uint256 _totalVotes); + + function votes(bytes32 _disputeId, address _voter) external view returns (uint256 _votes); + + /** + * @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); + + /// @inheritdoc IResolutionModule + function startResolution( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; + + /** + * @notice Casts a vote in favor of a dispute + * @param _numberOfVotes The number of votes to cast + */ + function castVote( + IOracle.Request calldata _request, + IOracle.Dispute calldata _dispute, + uint256 _numberOfVotes + ) external; + + /// @inheritdoc IResolutionModule + function resolveDispute( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; + + /** + * @notice Gets the voters of a dispute + * @param _disputeId The id of the dispute + * @return _voters The addresses of the voters + */ + function getVoters(bytes32 _disputeId) external view returns (address[] memory _voters); +} diff --git a/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol b/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol index da6dde87..dc87ae63 100644 --- a/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol +++ b/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol @@ -1,409 +1,400 @@ -// // 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 { -// ERC20ResolutionModule, -// IERC20ResolutionModule -// } from '../../../../contracts/modules/resolution/ERC20ResolutionModule.sol'; - -// contract ForTest_ERC20ResolutionModule is ERC20ResolutionModule { -// constructor(IOracle _oracle) ERC20ResolutionModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } - -// function forTest_setEscalation(bytes32 _disputeId, ERC20ResolutionModule.Escalation calldata __escalation) public { -// escalations[_disputeId] = __escalation; -// } - -// function forTest_setVotes(bytes32 _disputeId, address _voter, uint256 _amountOfVotes) public { -// votes[_disputeId][_voter] = _amountOfVotes; -// } -// } - -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_ERC20ResolutionModule public module; -// // A mock oracle -// IOracle public oracle; -// // A mock token -// IERC20 public token; -// // Mock EOA proposer -// address public proposer = makeAddr('proposer'); -// // Mock EOA disputer -// address public disputer = makeAddr('disputer'); - -// // Mocking module events -// event VoteCast(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); -// event VotingPhaseStarted(uint256 _startTime, bytes32 _disputeId); -// event DisputeResolved(bytes32 indexed _requestId, bytes32 indexed _disputeId, IOracle.DisputeStatus _status); - -// /** -// * @notice Deploy the target and mock oracle extension -// */ -// function setUp() public { -// oracle = IOracle(makeAddr('Oracle')); -// vm.etch(address(oracle), hex'069420'); - -// token = IERC20(makeAddr('ERC20')); -// vm.etch(address(token), hex'069420'); - -// module = new ForTest_ERC20ResolutionModule(oracle); -// } - -// /** -// * @dev Helper function to cast votes. -// */ -// function _populateVoters( -// bytes32 _requestId, -// bytes32 _disputeId, -// uint256 _amountOfVoters, -// uint256 _amountOfVotes -// ) internal returns (uint256 _totalVotesCast) { -// for (uint256 _i = 1; _i <= _amountOfVoters;) { -// vm.warp(120_000); -// vm.startPrank(vm.addr(_i)); -// vm.mockCall( -// address(token), -// abi.encodeCall(IERC20.transferFrom, (vm.addr(_i), address(module), _amountOfVotes)), -// abi.encode() -// ); -// module.castVote(_requestId, _disputeId, _amountOfVotes); -// vm.stopPrank(); -// _totalVotesCast += _amountOfVotes; -// unchecked { -// ++_i; -// } -// } -// } -// } - -// contract ERC20ResolutionModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleName() public { -// assertEq(module.moduleName(), 'ERC20ResolutionModule'); -// } - -// /** -// * @notice Test that the decodeRequestData function returns the correct values -// */ -// function test_decodeRequestData_returnsCorrectData( -// bytes32 _requestId, -// address _token, -// uint256 _minVotesForQuorum, -// uint256 _votingTimeWindow -// ) public { -// // Mock data -// bytes memory _requestData = abi.encode(_token, _minVotesForQuorum, _votingTimeWindow); - -// // Store the mock request -// module.forTest_setRequestData(_requestId, _requestData); - -// // Test: decode the given request data -// IERC20ResolutionModule.RequestParameters memory _params = module.decodeRequestData(_requestId); - -// // Check: decoded values match original values? -// assertEq(address(_params.votingToken), _token); -// assertEq(_params.minVotesForQuorum, _minVotesForQuorum); -// assertEq(_params.timeUntilDeadline, _votingTimeWindow); -// } -// } - -// contract ERC20ResolutionModule_Unit_StartResolution is BaseTest { -// /** -// * @notice Test that the `startResolution` is correctly called and the voting phase is started -// */ -// function test_startResolution(bytes32 _disputeId) public { -// // Check: does revert if called by address != oracle? -// vm.expectRevert(IModule.Module_OnlyOracle.selector); -// module.startResolution(_disputeId); - -// // Check: emits VotingPhaseStarted event? -// vm.expectEmit(true, true, true, true); -// emit VotingPhaseStarted(block.timestamp, _disputeId); - -// vm.prank(address(oracle)); -// module.startResolution(_disputeId); - -// (uint256 _startTime,) = module.escalations(_disputeId); - -// // Check: `startTime` is set to block.timestamp? -// assertEq(_startTime, block.timestamp); -// } -// } - -// contract ERC20ResolutionModule_Unit_CastVote is BaseTest { -// /** -// * @notice Test casting votes in valid voting time window. -// */ -// function test_castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _amountOfVotes, address _voter) public { -// // Store mock dispute -// IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Store mock escalation data with startTime 100_000 -// module.forTest_setEscalation( -// _disputeId, -// IERC20ResolutionModule.Escalation({ -// startTime: 100_000, -// totalVotes: 0 // Initial amount of votes -// }) -// ); - -// uint256 _minVotesForQuorum = 1; -// uint256 _votingTimeWindow = 40_000; - -// // Store mock request data with 40_000 voting time window -// module.forTest_setRequestData(_requestId, abi.encode(token, _minVotesForQuorum, _votingTimeWindow)); - -// // Mock and expect IERC20.transferFrom to be called -// _mockAndExpect( -// address(token), abi.encodeCall(IERC20.transferFrom, (_voter, address(module), _amountOfVotes)), abi.encode() -// ); - -// // Warp to voting phase -// vm.warp(130_000); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true); -// emit VoteCast(_voter, _disputeId, _amountOfVotes); - -// vm.prank(_voter); -// module.castVote(_requestId, _disputeId, _amountOfVotes); - -// (, uint256 _totalVotes) = module.escalations(_disputeId); -// // Check: totalVotes is updated? -// assertEq(_totalVotes, _amountOfVotes); - -// // Check: voter data is updated? -// assertEq(module.votes(_disputeId, _voter), _amountOfVotes); -// } - -// /** -// * @notice Test that `castVote` reverts if there is no dispute with the given`_disputeId` -// */ -// function test_revertIfNonExistentDispute(bytes32 _requestId, bytes32 _disputeId, uint256 _amountOfVotes) public { -// // Default non-existent dispute -// IOracle.Dispute memory _mockDispute = IOracle.Dispute({ -// disputer: address(0), -// responseId: bytes32(0), -// proposer: address(0), -// requestId: bytes32(0), -// status: IOracle.DisputeStatus.None, -// createdAt: 0 -// }); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Check: reverts if called with `_disputeId` of a non-existent dispute? -// vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_NonExistentDispute.selector); -// module.castVote(_requestId, _disputeId, _amountOfVotes); -// } - -// /** -// * @notice Test that `castVote` reverts if called with `_disputeId` of a non-escalated dispute. -// */ -// function test_revertIfNotEscalated(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes) public { -// // Mock the oracle response for looking up a dispute -// IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Check: reverts if called with `_disputeId` of a non-escalated dispute? -// vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_DisputeNotEscalated.selector); -// module.castVote(_requestId, _disputeId, _numberOfVotes); -// } - -// /** -// * @notice Test that `castVote` reverts if called with `_disputeId` of an already resolved dispute. -// */ -// function test_revertIfAlreadyResolved(bytes32 _requestId, bytes32 _disputeId, uint256 _amountOfVotes) public { -// // Mock dispute already resolved => DisputeStatus.Lost -// IOracle.Dispute memory _mockDispute = IOracle.Dispute({ -// disputer: disputer, -// responseId: bytes32('response'), -// proposer: proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.Lost, -// createdAt: block.timestamp -// }); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Check: reverts if dispute is already resolved? -// vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_AlreadyResolved.selector); -// module.castVote(_requestId, _disputeId, _amountOfVotes); -// } - -// /** -// * @notice Test that `castVote` reverts if called outside the voting time window. -// */ -// function test_revertIfVotingPhaseOver( -// bytes32 _requestId, -// bytes32 _disputeId, -// uint256 _numberOfVotes, -// uint256 _timestamp -// ) public { -// vm.assume(_timestamp > 140_000); - -// // Mock the oracle response for looking up a dispute -// IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); - -// // Mock and expect IOracle.getDispute to be called -// vm.mockCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); - -// // Store request data -// uint256 _minVotesForQuorum = 1; -// uint256 _votingTimeWindow = 40_000; - -// module.forTest_setRequestData(_requestId, abi.encode(token, _minVotesForQuorum, _votingTimeWindow)); - -// // Jump to timestamp -// vm.warp(_timestamp); - -// // Check: reverts if trying to cast vote after voting phase? -// vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_VotingPhaseOver.selector); -// module.castVote(_requestId, _disputeId, _numberOfVotes); -// } -// } - -// contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest { -// /** -// * @notice Test that a dispute is resolved, the tokens are transferred back to the voters and the dispute status updated. -// */ -// function test_resolveDispute(bytes32 _requestId, bytes32 _disputeId, uint16 _minVotesForQuorum) public { -// // Store mock dispute and mock calls -// IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); - -// // Mock and expect IOracle.getDispute to be called -// vm.mockCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Store request data -// uint256 _votingTimeWindow = 40_000; - -// module.forTest_setRequestData(_requestId, abi.encode(token, _minVotesForQuorum, _votingTimeWindow)); - -// // Store escalation data with `startTime` 100_000 and votes 0 -// module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); - -// uint256 _votersAmount = 5; - -// // Make 5 addresses cast 100 votes each -// uint256 _totalVotesCast = _populateVoters(_requestId, _disputeId, _votersAmount, 100); - -// // Warp to resolving phase -// vm.warp(150_000); - -// // Mock and expect token transfers (should happen always) -// for (uint256 _i = 1; _i <= _votersAmount;) { -// _mockAndExpect(address(token), abi.encodeCall(IERC20.transfer, (vm.addr(_i), 100)), abi.encode()); -// unchecked { -// ++_i; -// } -// } - -// // If quorum reached, check for dispute status update and event emission -// IOracle.DisputeStatus _newStatus = -// _totalVotesCast >= _minVotesForQuorum ? IOracle.DisputeStatus.Won : IOracle.DisputeStatus.Lost; - -// // Mock and expect IOracle.updateDisputeStatus to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.updateDisputeStatus, (_disputeId, _newStatus)), abi.encode()); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true); -// emit DisputeResolved(_requestId, _disputeId, _newStatus); - -// // Check: does revert if called by address != oracle? -// vm.expectRevert(IModule.Module_OnlyOracle.selector); -// module.resolveDispute(_disputeId); - -// vm.prank(address(oracle)); -// module.resolveDispute(_disputeId); -// } - -// /** -// * @notice Test that `resolveDispute` reverts if called during voting phase. -// */ -// function test_revertIfOnGoingVotePhase(bytes32 _requestId, bytes32 _disputeId, uint256 _timestamp) public { -// _timestamp = bound(_timestamp, 500_000, 999_999); - -// // Store mock dispute and mock calls -// IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// module.forTest_setEscalation( -// _disputeId, -// IERC20ResolutionModule.Escalation({ -// startTime: 500_000, -// totalVotes: 0 // Initial amount of votes -// }) -// ); - -// // Store request data -// uint256 _minVotesForQuorum = 1; -// uint256 _votingTimeWindow = 500_000; - -// module.forTest_setRequestData(_requestId, abi.encode(token, _minVotesForQuorum, _votingTimeWindow)); - -// // Jump to timestamp -// vm.warp(_timestamp); - -// // Check: reverts if trying to resolve during voting phase? -// vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_OnGoingVotingPhase.selector); -// vm.prank(address(oracle)); -// module.resolveDispute(_disputeId); -// } -// } - -// contract ERC20ResolutionModule_Unit_GetVoters is BaseTest { -// /** -// * @notice Test that `getVoters` returns an array of addresses of users that have voted. -// */ -// function test_getVoters(bytes32 _requestId, bytes32 _disputeId) public { -// // Store mock dispute and mock calls -// IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Store request data -// uint256 _votingTimeWindow = 40_000; -// uint256 _minVotesForQuorum = 1; - -// module.forTest_setRequestData(_requestId, abi.encode(token, _minVotesForQuorum, _votingTimeWindow)); - -// // Store escalation data with `startTime` 100_000 and votes 0 -// module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); - -// uint256 _votersAmount = 3; - -// // Make 3 addresses cast 100 votes each -// _populateVoters(_requestId, _disputeId, _votersAmount, 100); - -// address[] memory _votersArray = module.getVoters(_disputeId); - -// for (uint256 _i = 1; _i <= _votersAmount; _i++) { -// assertEq(_votersArray[_i - 1], vm.addr(_i)); -// } -// } -// } +// 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 { + ERC20ResolutionModule, + IERC20ResolutionModule +} from '../../../../contracts/modules/resolution/ERC20ResolutionModule.sol'; + +contract ForTest_ERC20ResolutionModule is ERC20ResolutionModule { + constructor(IOracle _oracle) ERC20ResolutionModule(_oracle) {} + + function forTest_setEscalation(bytes32 _disputeId, ERC20ResolutionModule.Escalation calldata __escalation) public { + escalations[_disputeId] = __escalation; + } + + function forTest_setVotes(bytes32 _disputeId, address _voter, uint256 _amountOfVotes) public { + votes[_disputeId][_voter] = _amountOfVotes; + } +} + +contract BaseTest is Test, Helpers { + // The target contract + ForTest_ERC20ResolutionModule public module; + // A mock oracle + IOracle public oracle; + // A mock token + IERC20 public token; + // Mock EOA proposer + address public proposer = makeAddr('proposer'); + // Mock EOA disputer + address public disputer = makeAddr('disputer'); + // Create a new dummy dispute + IOracle.Dispute public mockDispute; + // Create a new dummy response + IOracle.Response public mockResponse; + address internal _proposer = makeAddr('proposer'); + bytes32 public mockId = bytes32('69'); + + // Mocking module events + event VoteCast(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); + event VotingPhaseStarted(uint256 _startTime, bytes32 _disputeId); + event DisputeResolved(bytes32 indexed _requestId, bytes32 indexed _disputeId, IOracle.DisputeStatus _status); + + /** + * @notice Deploy the target and mock oracle extension + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + token = IERC20(makeAddr('ERC20')); + vm.etch(address(token), hex'069420'); + + module = new ForTest_ERC20ResolutionModule(oracle); + + mockDispute = + IOracle.Dispute({disputer: disputer, proposer: proposer, responseId: bytes32('69'), requestId: bytes32('69')}); + + mockResponse = IOracle.Response({proposer: _proposer, requestId: mockId, response: bytes('')}); + } + + /** + * @dev Helper function to cast votes. + */ + function _populateVoters( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _amountOfVoters, + uint256 _amountOfVotes, + IOracle.Request calldata _request + ) internal returns (uint256 _totalVotesCast) { + for (uint256 _i = 1; _i <= _amountOfVoters;) { + vm.warp(120_000); + vm.startPrank(vm.addr(_i)); + vm.mockCall( + address(token), + abi.encodeCall(IERC20.transferFrom, (vm.addr(_i), address(module), _amountOfVotes)), + abi.encode() + ); + module.castVote(_request, mockDispute, _amountOfVotes); + vm.stopPrank(); + _totalVotesCast += _amountOfVotes; + unchecked { + ++_i; + } + } + } +} + +contract ERC20ResolutionModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleName() public { + assertEq(module.moduleName(), 'ERC20ResolutionModule'); + } + + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData_returnsCorrectData( + bytes32 _requestId, + address _token, + uint256 _minVotesForQuorum, + uint256 _votingTimeWindow + ) public { + // Mock data + bytes memory _requestData = abi.encode(_token, _minVotesForQuorum, _votingTimeWindow); + + // Test: decode the given request data + IERC20ResolutionModule.RequestParameters memory _params = module.decodeRequestData(_requestData); + + // Check: decoded values match original values? + assertEq(address(_params.votingToken), _token); + assertEq(_params.minVotesForQuorum, _minVotesForQuorum); + assertEq(_params.timeUntilDeadline, _votingTimeWindow); + } +} + +contract ERC20ResolutionModule_Unit_StartResolution is BaseTest { + /** + * @notice Test that the `startResolution` is correctly called and the voting phase is started + */ + function test_startResolution(bytes32 _disputeId, IOracle.Request calldata _request) public { + // Check: does revert if called by address != oracle? + vm.expectRevert(IModule.Module_OnlyOracle.selector); + module.startResolution(_disputeId, _request, mockResponse, mockDispute); + + // Check: emits VotingPhaseStarted event? + vm.expectEmit(true, true, true, true); + emit VotingPhaseStarted(block.timestamp, _disputeId); + + vm.prank(address(oracle)); + module.startResolution(_disputeId, _request, mockResponse, mockDispute); + + (uint256 _startTime,) = module.escalations(_disputeId); + + // Check: `startTime` is set to block.timestamp? + assertEq(_startTime, block.timestamp); + } +} + +contract ERC20ResolutionModule_Unit_CastVote is BaseTest { + /** + * @notice Test casting votes in valid voting time window. + */ + function test_castVote( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _amountOfVotes, + address _voter, + IOracle.Request calldata _request + ) public { + // Store mock dispute + mockDispute.requestId = _getId(_request); + + // Store mock escalation data with startTime 100_000 + module.forTest_setEscalation( + _disputeId, + IERC20ResolutionModule.Escalation({ + startTime: 100_000, + totalVotes: 0 // Initial amount of votes + }) + ); + + uint256 _minVotesForQuorum = 1; + uint256 _votingTimeWindow = 40_000; + + // Mock and expect IERC20.transferFrom to be called + _mockAndExpect( + address(token), abi.encodeCall(IERC20.transferFrom, (_voter, address(module), _amountOfVotes)), abi.encode() + ); + + // Warp to voting phase + vm.warp(130_000); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true); + emit VoteCast(_voter, _disputeId, _amountOfVotes); + + vm.prank(_voter); + module.castVote(_request, mockDispute, _amountOfVotes); + + (, uint256 _totalVotes) = module.escalations(_disputeId); + // Check: totalVotes is updated? + assertEq(_totalVotes, _amountOfVotes); + + // Check: voter data is updated? + assertEq(module.votes(_disputeId, _voter), _amountOfVotes); + } + + /** + * @notice Test that `castVote` reverts if there is no dispute with the given`_disputeId` + */ + function test_revertIfNonExistentDispute( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _amountOfVotes, + IOracle.Request calldata _request + ) public { + // Default non-existent dispute + mockDispute.disputer = address(0); + mockDispute.responseId = bytes32(0); + mockDispute.proposer = address(0); + mockDispute.requestId = bytes32(0); + + // Check: reverts if called with `_disputeId` of a non-existent dispute? + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_NonExistentDispute.selector); + module.castVote(_request, mockDispute, _amountOfVotes); + } + + /** + * @notice Test that `castVote` reverts if called with `_disputeId` of a non-escalated dispute. + */ + function test_revertIfNotEscalated( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _numberOfVotes, + IOracle.Request calldata _request + ) public { + // Mock the oracle response for looking up a dispute + mockDispute.requestId = _requestId; + + // Check: reverts if called with `_disputeId` of a non-escalated dispute? + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_DisputeNotEscalated.selector); + module.castVote(_request, mockDispute, _numberOfVotes); + } + + /** + * @notice Test that `castVote` reverts if called with `_disputeId` of an already resolved dispute. + */ + function test_revertIfAlreadyResolved( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _amountOfVotes, + IOracle.Request calldata _request + ) public { + // Mock dispute already resolved => DisputeStatus.Lost + mockDispute.requestId = _requestId; + + // Check: reverts if dispute is already resolved? + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_AlreadyResolved.selector); + module.castVote(_request, mockDispute, _amountOfVotes); + } + + /** + * @notice Test that `castVote` reverts if called outside the voting time window. + */ + function test_revertIfVotingPhaseOver( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _numberOfVotes, + uint256 _timestamp, + IOracle.Request calldata _request + ) public { + vm.assume(_timestamp > 140_000); + + // Mock the oracle response for looking up a dispute + mockDispute.requestId = _requestId; + + module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); + + // Store request data + uint256 _minVotesForQuorum = 1; + uint256 _votingTimeWindow = 40_000; + + // Jump to timestamp + vm.warp(_timestamp); + + // Check: reverts if trying to cast vote after voting phase? + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_VotingPhaseOver.selector); + module.castVote(_request, mockDispute, _numberOfVotes); + } +} + +contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest { + /** + * @notice Test that a dispute is resolved, the tokens are transferred back to the voters and the dispute status updated. + */ + function test_resolveDispute(IOracle.Request calldata _request, bytes32 _disputeId, uint16 _minVotesForQuorum) public { + // Store mock dispute and mock calls + bytes32 _requestId = _getId(_request); + mockDispute.requestId = _requestId; + + // Store request data + uint256 _votingTimeWindow = 40_000; + + // Store escalation data with `startTime` 100_000 and votes 0 + module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); + + uint256 _votersAmount = 5; + + // Make 5 addresses cast 100 votes each + uint256 _totalVotesCast = _populateVoters(_requestId, _disputeId, _votersAmount, 100, _request); + + // Warp to resolving phase + vm.warp(150_000); + + // Mock and expect token transfers (should happen always) + for (uint256 _i = 1; _i <= _votersAmount;) { + _mockAndExpect(address(token), abi.encodeCall(IERC20.transfer, (vm.addr(_i), 100)), abi.encode()); + unchecked { + ++_i; + } + } + + // If quorum reached, check for dispute status update and event emission + IOracle.DisputeStatus _newStatus = + _totalVotesCast >= _minVotesForQuorum ? IOracle.DisputeStatus.Won : IOracle.DisputeStatus.Lost; + + // Mock and expect IOracle.updateDisputeStatus to be called + _mockAndExpect( + address(oracle), + abi.encodeCall(IOracle.updateDisputeStatus, (_request, mockResponse, mockDispute, _newStatus)), + abi.encode() + ); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true); + emit DisputeResolved(_requestId, _disputeId, _newStatus); + + // Check: does revert if called by address != oracle? + vm.expectRevert(IModule.Module_OnlyOracle.selector); + module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + + vm.prank(address(oracle)); + module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + } + + /** + * @notice Test that `resolveDispute` reverts if called during voting phase. + */ + function test_revertIfOnGoingVotePhase( + IOracle.Request calldata _request, + bytes32 _disputeId, + uint256 _timestamp + ) public { + _timestamp = bound(_timestamp, 500_000, 999_999); + + // Store mock dispute and mock calls + bytes32 _requestId = _getId(_request); + mockDispute.requestId = _requestId; + + module.forTest_setEscalation( + _disputeId, + IERC20ResolutionModule.Escalation({ + startTime: 500_000, + totalVotes: 0 // Initial amount of votes + }) + ); + + // Store request data + uint256 _minVotesForQuorum = 1; + uint256 _votingTimeWindow = 500_000; + + // Jump to timestamp + vm.warp(_timestamp); + + // Check: reverts if trying to resolve during voting phase? + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_OnGoingVotingPhase.selector); + vm.prank(address(oracle)); + module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + } +} + +contract ERC20ResolutionModule_Unit_GetVoters is BaseTest { + /** + * @notice Test that `getVoters` returns an array of addresses of users that have voted. + */ + function test_getVoters(IOracle.Request calldata _request, bytes32 _disputeId) public { + // Store mock dispute and mock calls + bytes32 _requestId = _getId(_request); + mockDispute.requestId = _requestId; + + // Store request data + uint256 _votingTimeWindow = 40_000; + uint256 _minVotesForQuorum = 1; + + // Store escalation data with `startTime` 100_000 and votes 0 + module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); + uint256 _votersAmount = 3; + + // Make 3 addresses cast 100 votes each + _populateVoters(_requestId, _disputeId, _votersAmount, 100, _request); + + address[] memory _votersArray = module.getVoters(_disputeId); + + for (uint256 _i = 1; _i <= _votersAmount; _i++) { + assertEq(_votersArray[_i - 1], vm.addr(_i)); + } + } +} From 761589ba4dd3370f95c7f981dc84ed153429d100 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:38:29 +0400 Subject: [PATCH 15/53] feat: uncomment helpers --- .../extensions/IAccountingExtension.sol | 380 +++++++++--------- solidity/test/utils/Helpers.sol | 122 +++--- 2 files changed, 256 insertions(+), 246 deletions(-) diff --git a/solidity/interfaces/extensions/IAccountingExtension.sol b/solidity/interfaces/extensions/IAccountingExtension.sol index 5a42243e..74c9c9cd 100644 --- a/solidity/interfaces/extensions/IAccountingExtension.sol +++ b/solidity/interfaces/extensions/IAccountingExtension.sol @@ -1,190 +1,190 @@ -// // 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'; - -// /* -// * @title AccountingExtension -// * @notice Extension allowing users to deposit and bond funds -// * to be used for payments and disputes. -// */ -// interface IAccountingExtension { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice A user deposited tokens into the accounting extension -// * @param _depositor The user who deposited the tokens -// * @param _token The address of the token deposited by the user -// * @param _amount The amount of `_token` deposited -// */ -// event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A user withdrew tokens from the accounting extension -// * @param _withdrawer The user who withdrew the tokens -// * @param _token The address of the token withdrawn by the user -// * @param _amount The amount of `_token` withdrawn -// */ -// event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A payment between users has been made -// * @param _beneficiary The user receiving the tokens -// * @param _payer The user who is getting its tokens transferred -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` transferred -// */ -// event Paid( -// bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount -// ); - -// /** -// * @notice User's funds have been bonded -// * @param _bonder The user who is getting its tokens bonded -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` bonded -// */ -// event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice User's funds have been released -// * @param _beneficiary The user who is getting its tokens released -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` released -// */ -// event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Thrown when the account doesn't have enough balance to bond/withdraw -// * or not enough bonded to release/pay -// */ -// error AccountingExtension_InsufficientFunds(); - -// /** -// * @notice Thrown when the module bonding user tokens hasn't been approved by the user. -// */ -// error AccountingExtension_InsufficientAllowance(); - -// /** -// * @notice Thrown when an `onlyAllowedModule` function is called by something -// * else than a module being used in the corresponding request -// */ -// error AccountingExtension_UnauthorizedModule(); - -// /** -// * @notice Thrown when an `onlyParticipant` function is called with an address -// * that is not part of the request. -// */ -// error AccountingExtension_UnauthorizedUser(); - -// /*/////////////////////////////////////////////////////////////// -// VARIABLES -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Returns the interface for the Oracle contract -// */ -// function ORACLE() external view returns (IOracle _oracle); - -// /** -// * @notice Returns the amount of a token a user has bonded -// * @param _user The address of the user with bonded tokens -// * @param _bondToken The token bonded -// * @param _requestId The id of the request the user bonded for -// * @return _amount The amount of `_bondToken` bonded -// */ -// function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); - -// /** -// * @notice Returns the amount of a token a user has deposited -// * @param _user The address of the user with deposited tokens -// * @param _token The token deposited -// * @return _amount The amount of `_token` deposited -// */ -// function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Transfers tokens from a user and updates his virtual balance -// * @dev The user must have approved the accounting extension to transfer the tokens. -// * @param _token The address of the token being deposited -// * @param _amount The amount of `_token` to deposit -// */ -// function deposit(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows an user to withdraw deposited tokens -// * @param _token The address of the token being withdrawn -// * @param _amount The amount of `_token` to withdraw -// */ -// function withdraw(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to transfer bonded tokens from one user to another -// * @dev Only the virtual balances in the accounting extension are modified. The token contract -// * is not called nor its balances modified. -// * @param _requestId The id of the request handling the user's tokens -// * @param _payer The address of the user paying the tokens -// * @param _receiver The address of the user receiving the tokens -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` being transferred -// */ -// function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a valid module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// * @param _sender The address starting the propose call on the Oracle -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; - -// /** -// * @notice Allows a valid module to release a user's tokens -// * @param _bonder The address of the user to release tokens for -// * @param _requestId The id of the request where the tokens were bonded -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` to release -// */ -// function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a user to approve a module for bonding tokens -// * @param _module The address of the module to be approved -// */ -// function approveModule(address _module) external; - -// /** -// * @notice Allows a user to revoke a module's approval for bonding tokens -// * @param _module The address of the module to be revoked -// */ -// function revokeModule(address _module) external; - -// /** -// * @notice Returns a list of all modules a user has approved -// * @param _user The address of the user -// * @return _approvedModules The array of all modules approved by the user -// */ -// function approvedModules(address _user) external view returns (address[] memory _approvedModules); -// } +// 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'; + +/* + * @title AccountingExtension + * @notice Extension allowing users to deposit and bond funds + * to be used for payments and disputes. + */ +interface IAccountingExtension { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A user deposited tokens into the accounting extension + * @param _depositor The user who deposited the tokens + * @param _token The address of the token deposited by the user + * @param _amount The amount of `_token` deposited + */ + event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A user withdrew tokens from the accounting extension + * @param _withdrawer The user who withdrew the tokens + * @param _token The address of the token withdrawn by the user + * @param _amount The amount of `_token` withdrawn + */ + event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A payment between users has been made + * @param _beneficiary The user receiving the tokens + * @param _payer The user who is getting its tokens transferred + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` transferred + */ + event Paid( + bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount + ); + + /** + * @notice User's funds have been bonded + * @param _bonder The user who is getting its tokens bonded + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` bonded + */ + event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); + + /** + * @notice User's funds have been released + * @param _beneficiary The user who is getting its tokens released + * @param _token The address of the token being released + * @param _amount The amount of `_token` released + */ + event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Thrown when the account doesn't have enough balance to bond/withdraw + * or not enough bonded to release/pay + */ + error AccountingExtension_InsufficientFunds(); + + /** + * @notice Thrown when the module bonding user tokens hasn't been approved by the user. + */ + error AccountingExtension_InsufficientAllowance(); + + /** + * @notice Thrown when an `onlyAllowedModule` function is called by something + * else than a module being used in the corresponding request + */ + error AccountingExtension_UnauthorizedModule(); + + /** + * @notice Thrown when an `onlyParticipant` function is called with an address + * that is not part of the request. + */ + error AccountingExtension_UnauthorizedUser(); + + /*/////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the interface for the Oracle contract + */ + function ORACLE() external view returns (IOracle _oracle); + + /** + * @notice Returns the amount of a token a user has bonded + * @param _user The address of the user with bonded tokens + * @param _bondToken The token bonded + * @param _requestId The id of the request the user bonded for + * @return _amount The amount of `_bondToken` bonded + */ + function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); + + /** + * @notice Returns the amount of a token a user has deposited + * @param _user The address of the user with deposited tokens + * @param _token The token deposited + * @return _amount The amount of `_token` deposited + */ + function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Transfers tokens from a user and updates his virtual balance + * @dev The user must have approved the accounting extension to transfer the tokens. + * @param _token The address of the token being deposited + * @param _amount The amount of `_token` to deposit + */ + function deposit(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows an user to withdraw deposited tokens + * @param _token The address of the token being withdrawn + * @param _amount The amount of `_token` to withdraw + */ + function withdraw(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to transfer bonded tokens from one user to another + * @dev Only the virtual balances in the accounting extension are modified. The token contract + * is not called nor its balances modified. + * @param _requestId The id of the request handling the user's tokens + * @param _payer The address of the user paying the tokens + * @param _receiver The address of the user receiving the tokens + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` being transferred + */ + function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a valid module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + * @param _sender The address starting the propose call on the Oracle + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; + + /** + * @notice Allows a valid module to release a user's tokens + * @param _bonder The address of the user to release tokens for + * @param _requestId The id of the request where the tokens were bonded + * @param _token The address of the token being released + * @param _amount The amount of `_token` to release + */ + function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a user to approve a module for bonding tokens + * @param _module The address of the module to be approved + */ + function approveModule(address _module) external; + + /** + * @notice Allows a user to revoke a module's approval for bonding tokens + * @param _module The address of the module to be revoked + */ + function revokeModule(address _module) external; + + /** + * @notice Returns a list of all modules a user has approved + * @param _user The address of the user + * @return _approvedModules The array of all modules approved by the user + */ + function approvedModules(address _user) external view returns (address[] memory _approvedModules); +} diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 46874489..af32e439 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -1,56 +1,66 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; - -// import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; - -// contract Helpers is DSTestPlus { -// modifier assumeFuzzable(address _address) { -// _assumeFuzzable(_address); -// _; -// } - -// function _assumeFuzzable(address _address) internal pure { -// assumeNotForgeAddress(_address); -// assumeNotZeroAddress(_address); -// assumeNotPrecompile(_address); -// } - -// function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { -// vm.mockCall(_receiver, _calldata, _returned); -// vm.expectCall(_receiver, _calldata); -// } - -// function _getMockDispute( -// bytes32 _requestId, -// address _disputer, -// address _proposer -// ) internal view returns (IOracle.Dispute memory _dispute) { -// _dispute = IOracle.Dispute({ -// disputer: _disputer, -// responseId: bytes32('response'), -// proposer: _proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.None, -// createdAt: block.timestamp -// }); -// } - -// function _forBondDepositERC20( -// IAccountingExtension _accountingExtension, -// address _depositor, -// IERC20 _token, -// uint256 _depositAmount, -// uint256 _balanceIncrease -// ) internal { -// vm.assume(_balanceIncrease >= _depositAmount); -// deal(address(_token), _depositor, _balanceIncrease); -// vm.startPrank(_depositor); -// _token.approve(address(_accountingExtension), _depositAmount); -// _accountingExtension.deposit(_token, _depositAmount); -// vm.stopPrank(); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; + +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; + +contract Helpers is DSTestPlus { + modifier assumeFuzzable(address _address) { + _assumeFuzzable(_address); + _; + } + + function _assumeFuzzable(address _address) internal pure { + assumeNotForgeAddress(_address); + assumeNotZeroAddress(_address); + assumeNotPrecompile(_address); + } + + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + function _getMockDispute( + bytes32 _requestId, + address _disputer, + address _proposer + ) internal view returns (IOracle.Dispute memory _dispute) { + _dispute = IOracle.Dispute({ + disputer: _disputer, + responseId: bytes32('response'), + proposer: _proposer, + requestId: _requestId + }); + } + + function _forBondDepositERC20( + IAccountingExtension _accountingExtension, + address _depositor, + IERC20 _token, + uint256 _depositAmount, + uint256 _balanceIncrease + ) internal { + vm.assume(_balanceIncrease >= _depositAmount); + deal(address(_token), _depositor, _balanceIncrease); + vm.startPrank(_depositor); + _token.approve(address(_accountingExtension), _depositAmount); + _accountingExtension.deposit(_token, _depositAmount); + vm.stopPrank(); + } + + function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_response)); + } + + function _getId(IOracle.Request memory _request) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_request)); + } + + function _getId(IOracle.Dispute memory _dispute) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_dispute)); + } +} From 8e3f8fa8bb2b1998c036a8a98a90f49d3c40b967 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 22:12:26 +0400 Subject: [PATCH 16/53] perf: optimize `MultipleCallbacksModule` --- .../finality/multiple_callbacks_module.md | 4 +- .../finality/MultipleCallbacksModule.sol | 100 +++---- .../finality/IMultipleCallbacksModule.sol | 131 +++++---- .../finality/MultipleCallbacksModule.t.sol | 263 +++++++----------- 4 files changed, 213 insertions(+), 285 deletions(-) diff --git a/docs/src/content/modules/finality/multiple_callbacks_module.md b/docs/src/content/modules/finality/multiple_callbacks_module.md index e9412209..a1bef269 100644 --- a/docs/src/content/modules/finality/multiple_callbacks_module.md +++ b/docs/src/content/modules/finality/multiple_callbacks_module.md @@ -10,8 +10,8 @@ The `MultipleCallbacksModule` is a finality module that allows users to make mul ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: Returns the decoded data for a request. The returned data includes the target addresses for the callback and the calldata forwarded to the targets. -- `finalizeRequest(bytes32 _requestId, address)`: Finalizes the request by executing the callback calls on the targets. +- `decodeRequestData(bytes calldata _data)`: Returns the decoded data for a request. The returned data includes the target addresses for the callback and the calldata forwarded to the targets. +- `finalizeRequest`: Finalizes the request by executing the callback calls on the targets. ### Request Parameters diff --git a/solidity/contracts/modules/finality/MultipleCallbacksModule.sol b/solidity/contracts/modules/finality/MultipleCallbacksModule.sol index 77d8a311..b2406538 100644 --- a/solidity/contracts/modules/finality/MultipleCallbacksModule.sol +++ b/solidity/contracts/modules/finality/MultipleCallbacksModule.sol @@ -1,58 +1,42 @@ -// // 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 {IMultipleCallbacksModule} from '../../../interfaces/modules/finality/IMultipleCallbacksModule.sol'; - -// contract MultipleCallbacksModule is Module, IMultipleCallbacksModule { -// constructor(IOracle _oracle) Module(_oracle) {} - -// /// @inheritdoc IMultipleCallbacksModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } - -// /// @inheritdoc IModule -// function moduleName() public pure returns (string memory _moduleName) { -// _moduleName = 'MultipleCallbacksModule'; -// } - -// /** -// * @notice Checks if the target addresses have code and the calldata amount matches the targets amount -// * @param _data The ABI encoded address of the target contracts and the calldata to be executed -// */ -// function _afterSetupRequest(bytes32, bytes calldata _data) internal view override { -// RequestParameters memory _params = abi.decode(_data, (RequestParameters)); -// uint256 _length = _params.targets.length; -// if (_length != _params.data.length) revert MultipleCallbackModule_InvalidParameters(); - -// for (uint256 _i; _i < _length;) { -// if (_params.targets[_i].code.length == 0) revert MultipleCallbackModule_TargetHasNoCode(); -// unchecked { -// ++_i; -// } -// } -// } - -// /// @inheritdoc IMultipleCallbacksModule -// function finalizeRequest( -// bytes32 _requestId, -// address _finalizer -// ) external override(IMultipleCallbacksModule, Module) onlyOracle { -// RequestParameters memory _params = decodeRequestData(_requestId); -// uint256 _length = _params.targets.length; - -// for (uint256 _i; _i < _length;) { -// _params.targets[_i].call(_params.data[_i]); -// emit Callback(_requestId, _params.targets[_i], _params.data[_i]); -// unchecked { -// ++_i; -// } -// } - -// emit RequestFinalized(_requestId, _finalizer); -// } -// } +// 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 {IMultipleCallbacksModule} from '../../../interfaces/modules/finality/IMultipleCallbacksModule.sol'; + +contract MultipleCallbacksModule is Module, IMultipleCallbacksModule { + constructor(IOracle _oracle) Module(_oracle) {} + + /// @inheritdoc IMultipleCallbacksModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } + + /// @inheritdoc IModule + function moduleName() public pure returns (string memory _moduleName) { + _moduleName = 'MultipleCallbacksModule'; + } + + /// @inheritdoc IMultipleCallbacksModule + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external override(IMultipleCallbacksModule, Module) onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.finalityModuleData); + uint256 _length = _params.targets.length; + + for (uint256 _i; _i < _length;) { + _params.targets[_i].call(_params.data[_i]); + emit Callback(_response.requestId, _params.targets[_i], _params.data[_i]); + unchecked { + ++_i; + } + } + + emit RequestFinalized(_response.requestId, _response, _finalizer); + } +} diff --git a/solidity/interfaces/modules/finality/IMultipleCallbacksModule.sol b/solidity/interfaces/modules/finality/IMultipleCallbacksModule.sol index f6de01cc..c9c0b456 100644 --- a/solidity/interfaces/modules/finality/IMultipleCallbacksModule.sol +++ b/solidity/interfaces/modules/finality/IMultipleCallbacksModule.sol @@ -1,69 +1,62 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IFinalityModule} from -// '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/finality/IFinalityModule.sol'; - -// /** -// * @title MultipleCallbackModule -// * @notice Module allowing users to make multiple calls to different contracts -// * as a result of a request being finalized. -// */ -// interface IMultipleCallbacksModule is IFinalityModule { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice A callback has been executed -// * @param _requestId The id of the request being finalized -// * @param _target The target address for the callback -// * @param _data The calldata forwarded to the _target -// */ -// event Callback(bytes32 indexed _requestId, address indexed _target, bytes _data); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Thrown when then target address has no code (i.e. is not a contract) -// */ -// error MultipleCallbackModule_TargetHasNoCode(); - -// /** -// * @notice Thrown when the targets array and the data array have different lengths -// */ -// error MultipleCallbackModule_InvalidParameters(); - -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Parameters of the request as stored in the module -// * @param targets The target addresses for the callback -// * @param data The calldata forwarded to the targets -// */ -// struct RequestParameters { -// address[] targets; -// bytes[] data; -// } -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @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 Finalizes the request by executing the callback calls on the targets -// * @dev The success of the callback calls is purposely not checked -// * @param _requestId The id of the request -// */ -// function finalizeRequest(bytes32 _requestId, address) external; -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IFinalityModule} from + '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/finality/IFinalityModule.sol'; + +/** + * @title MultipleCallbackModule + * @notice Module allowing users to make multiple calls to different contracts + * as a result of a request being finalized. + */ +interface IMultipleCallbacksModule is IFinalityModule { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A callback has been executed + * @param _requestId The id of the request being finalized + * @param _target The target address for the callback + * @param _data The calldata forwarded to the _target + */ + event Callback(bytes32 indexed _requestId, address indexed _target, bytes _data); + + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Parameters of the request as stored in the module + * @param targets The target addresses for the callback + * @param data The calldata forwarded to the targets + */ + struct RequestParameters { + address[] targets; + bytes[] data; + } + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @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 the request by executing the callback calls on the targets + * @dev The success of the callback calls is purposely not checked + * @param _request The request being finalized + * @param _response The response + * @param _finalizer The address finalizing the request + */ + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external; +} diff --git a/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol b/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol index 3e888cf2..be721d19 100644 --- a/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol +++ b/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol @@ -1,156 +1,107 @@ -// // SPDX-License-Identifier: AGPL-3.0-only -// pragma solidity ^0.8.19; - -// import 'forge-std/Test.sol'; - -// import {Helpers} from '../../../utils/Helpers.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 { -// MultipleCallbacksModule, -// IMultipleCallbacksModule -// } from '../../../../contracts/modules/finality/MultipleCallbacksModule.sol'; - -// /** -// * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks -// */ -// contract ForTest_MultipleCallbacksModule is MultipleCallbacksModule { -// constructor(IOracle _oracle) MultipleCallbacksModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, address[] calldata _targets, bytes[] calldata _data) public { -// requestData[_requestId] = abi.encode(IMultipleCallbacksModule.RequestParameters({targets: _targets, data: _data})); -// } -// } - -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_MultipleCallbacksModule public multipleCallbackModule; -// // A mock oracle -// IOracle public oracle; - -// event Callback(bytes32 indexed _request, address indexed _target, bytes _data); -// 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'); - -// multipleCallbackModule = new ForTest_MultipleCallbacksModule(oracle); -// } -// } - -// /** -// * @title MultipleCallback Module Unit tests -// */ -// contract MultipleCallbacksModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(multipleCallbackModule.moduleName(), 'MultipleCallbacksModule'); -// } -// } - -// contract MultipleCallbacksModule_Unit_FinalizeRequests is BaseTest { -// /** -// * @notice Test that finalizeRequests calls the _target.callback with the correct data -// */ -// function test_finalizeRequest(bytes32 _requestId, address[1] calldata _targets, bytes[1] calldata __data) public { -// address _target = _targets[0]; -// bytes calldata _data = __data[0]; - -// assumeNotPrecompile(_target); -// vm.assume(_target != address(vm)); - -// // Create and set some mock request data -// address[] memory _targetParams = new address[](1); -// _targetParams[0] = _targets[0]; -// bytes[] memory _dataParams = new bytes[](1); -// _dataParams[0] = __data[0]; -// multipleCallbackModule.forTest_setRequestData(_requestId, _targetParams, _dataParams); - -// _mockAndExpect(_target, _data, abi.encode()); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(multipleCallbackModule)); -// emit Callback(_requestId, _target, _data); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(multipleCallbackModule)); -// emit RequestFinalized(_requestId, address(oracle)); - -// vm.prank(address(oracle)); -// multipleCallbackModule.finalizeRequest(_requestId, address(oracle)); -// } - -// /** -// * @notice Test that the finalizeRequests 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(IModule.Module_OnlyOracle.selector); -// vm.prank(_caller); -// multipleCallbackModule.finalizeRequest(_requestId, address(_caller)); -// } - -// function test_revertIfInvalidParameters(bytes32 _requestId, address[] memory _targets, bytes[] memory _data) public { -// vm.assume(_targets.length != _data.length); - -// bytes memory _requestData = abi.encode(IMultipleCallbacksModule.RequestParameters({targets: _targets, data: _data})); - -// // Check: does it revert if arrays length mismatch? -// vm.expectRevert(IMultipleCallbacksModule.MultipleCallbackModule_InvalidParameters.selector); -// vm.prank(address(oracle)); -// multipleCallbackModule.setupRequest(_requestId, _requestData); -// } -// } - -// contract MultipleCallbacksModule_Unit_Setup is BaseTest { -// function test_revertsIfInvalidTarget(bytes32 _requestId, address[] memory _targets, bytes memory _data) public { -// vm.assume(_targets.length > 1); - -// // Hardcoding data (as it is not the case tested) to avoid vm.assume issues -// bytes[] memory _targetData = new bytes[](_targets.length); -// for (uint256 _i = 0; _i < _targets.length; _i++) { -// _targetData[_i] = abi.encodeWithSelector(bytes4(keccak256('callback(bytes32,bytes)')), _requestId, _data); -// } - -// bytes memory _requestData = -// abi.encode(IMultipleCallbacksModule.RequestParameters({targets: _targets, data: _targetData})); - -// // Check: does it revert if the target has no code? -// vm.expectRevert(IMultipleCallbacksModule.MultipleCallbackModule_TargetHasNoCode.selector); -// vm.prank(address(oracle)); -// multipleCallbackModule.setupRequest(_requestId, _requestData); -// } - -// function test_setUpMultipleTargets(bytes32 _requestId, address[] memory _targets, bytes memory _data) public { -// vm.assume(_targets.length > 1); - -// // Hardcoding data (as it is not the case tested) to avoid vm.assume issues -// bytes[] memory _targetData = new bytes[](_targets.length); -// for (uint256 _i = 0; _i < _targets.length; _i++) { -// _assumeFuzzable(_targets[_i]); -// vm.etch(_targets[_i], hex'069420'); -// _targetData[_i] = abi.encodeWithSelector(bytes4(keccak256('callback(bytes32,bytes)')), _requestId, _data); -// } - -// bytes memory _requestData = -// abi.encode(IMultipleCallbacksModule.RequestParameters({targets: _targets, data: _targetData})); - -// vm.prank(address(oracle)); -// multipleCallbackModule.setupRequest(_requestId, _requestData); - -// IMultipleCallbacksModule.RequestParameters memory _storedParams = -// multipleCallbackModule.decodeRequestData(_requestId); -// // Check: is the data properly stored? -// assertEq(_storedParams.targets, _targets); -// } -// } +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; + +import {Helpers} from '../../../utils/Helpers.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 { + MultipleCallbacksModule, + IMultipleCallbacksModule +} from '../../../../contracts/modules/finality/MultipleCallbacksModule.sol'; + +contract BaseTest is Test, Helpers { + // The target contract + MultipleCallbacksModule public multipleCallbackModule; + // A mock oracle + IOracle public oracle; + // Mock EOA proposer + address public proposer = makeAddr('proposer'); + // Mock EOA disputer + address public disputer = makeAddr('disputer'); + // Create a new dummy dispute + IOracle.Dispute public mockDispute; + // Create a new dummy response + IOracle.Response public mockResponse; + bytes32 public mockId = bytes32('69'); + + event Callback(bytes32 indexed _request, address indexed _target, bytes _data); + 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'); + + multipleCallbackModule = new MultipleCallbacksModule(oracle); + mockDispute = + IOracle.Dispute({disputer: disputer, proposer: proposer, responseId: bytes32('69'), requestId: bytes32('69')}); + mockResponse = IOracle.Response({proposer: proposer, requestId: mockId, response: bytes('')}); + } +} + +/** + * @title MultipleCallback Module Unit tests + */ +contract MultipleCallbacksModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(multipleCallbackModule.moduleName(), 'MultipleCallbacksModule'); + } +} + +contract MultipleCallbacksModule_Unit_FinalizeRequests is BaseTest { + /** + * @notice Test that finalizeRequests calls the _target.callback with the correct data + */ + function test_finalizeRequest( + IOracle.Request calldata _request, + address[1] calldata _targets, + bytes[1] calldata __data + ) public { + bytes32 _requestId = _getId(_request); + address _target = _targets[0]; + bytes calldata _data = __data[0]; + + assumeNotPrecompile(_target); + vm.assume(_target != address(vm)); + + // Create and set some mock request data + address[] memory _targetParams = new address[](1); + _targetParams[0] = _targets[0]; + bytes[] memory _dataParams = new bytes[](1); + _dataParams[0] = __data[0]; + + _mockAndExpect(_target, _data, abi.encode()); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(multipleCallbackModule)); + emit Callback(_requestId, _target, _data); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(multipleCallbackModule)); + emit RequestFinalized(_requestId, address(oracle)); + + vm.prank(address(oracle)); + multipleCallbackModule.finalizeRequest(_request, mockResponse, address(oracle)); + } + + /** + * @notice Test that the finalizeRequests reverts if caller is not the oracle + */ + function test_revertsIfWrongCaller(IOracle.Request calldata _request, address _caller) public { + vm.assume(_caller != address(oracle)); + + // Check: does it revert if not called by the Oracle? + vm.expectRevert(IModule.Module_OnlyOracle.selector); + vm.prank(_caller); + multipleCallbackModule.finalizeRequest(_request, mockResponse, address(_caller)); + } +} From 08671215fb9e6d3bfd03d86bad0a078e71cf4b04 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 22:12:42 +0400 Subject: [PATCH 17/53] feat: add `getId` helpers --- solidity/test/utils/Helpers.sol | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 5d7691cf..af32e439 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -51,4 +51,16 @@ contract Helpers is DSTestPlus { _accountingExtension.deposit(_token, _depositAmount); vm.stopPrank(); } + + function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_response)); + } + + function _getId(IOracle.Request memory _request) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_request)); + } + + function _getId(IOracle.Dispute memory _dispute) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_dispute)); + } } From 5d90fcc3ed1a58de4738f0bb1a3a486e2f9edea3 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Fri, 10 Nov 2023 15:57:38 +0400 Subject: [PATCH 18/53] feat: update `prophet-core` package --- package.json | 2 +- yarn.lock | 78 +++++++++++++++++++++++++--------------------------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index b7700bc1..dcb5ecad 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "package.json": "sort-package-json" }, "dependencies": { - "@defi-wonderland/prophet-core-contracts": "0.0.0-d05a00d0", + "@defi-wonderland/prophet-core-contracts": "0.0.0-a1d2cc55", "@defi-wonderland/solidity-utils": "0.0.0-3e9c8e8b", "@openzeppelin/contracts": "^4.9.3", "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", diff --git a/yarn.lock b/yarn.lock index c61b7709..658b1212 100644 --- a/yarn.lock +++ b/yarn.lock @@ -192,10 +192,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@defi-wonderland/prophet-core-contracts@0.0.0-d05a00d0": - version "0.0.0-d05a00d0" - resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-d05a00d0.tgz#1357d917fe46a5a12faa67f557e990255dda14fd" - integrity sha512-F/y0r/qDLFACzsN7Y2VRAPIS9Yhx2btU/m7cQT7T84TbIxAmBGVw6/7nb+HeIbXh+QDO90RP6vHAdQOow/q1Xw== +"@defi-wonderland/prophet-core-contracts@0.0.0-a1d2cc55": + version "0.0.0-a1d2cc55" + resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-a1d2cc55.tgz#e0bba63cdb143ffba6721049d2b0577eb39329fb" + integrity sha512-gl+8QvkzPd144yESzXhl2ceJ5blZczKh7HLioSfJ1uZnrJAR90z/MJD1fCb/1Q3oDyaq6WMWnJSK9VvMkadTtQ== dependencies: "@defi-wonderland/solidity-utils" "0.0.0-3e9c8e8b" "@openzeppelin/contracts" "^4.9.3" @@ -274,9 +274,9 @@ antlr4ts "^0.5.0-alpha.4" "@solidity-parser/parser@^0.16.0": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.1.tgz#f7c8a686974e1536da0105466c4db6727311253c" - integrity sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw== + version "0.16.2" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.2.tgz#42cb1e3d88b3e8029b0c9befff00b634cd92d2fa" + integrity sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg== dependencies: antlr4ts "^0.5.0-alpha.4" @@ -324,9 +324,9 @@ ts-essentials "^7.0.1" "@types/minimist@^1.2.0": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.4.tgz#81f886786411c45bba3f33e781ab48bd56bfca2e" - integrity sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@20.5.1": version "20.5.1" @@ -334,9 +334,9 @@ integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== "@types/normalize-package-data@^2.4.0": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz#291c243e4b94dbfbc0c0ee26b7666f1d5c030e2c" - integrity sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/prettier@^2.1.1": version "2.7.3" @@ -357,9 +357,9 @@ acorn-jsx@^5.0.0: integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== acorn@^6.0.7: version "6.4.2" @@ -367,9 +367,9 @@ acorn@^6.0.7: integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^8.4.1: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== add-stream@^1.0.0: version "1.0.0" @@ -1181,14 +1181,10 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -"ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": - version "1.0.0" - resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" - -"ds-test@https://github.com/dapphub/ds-test": +"ds-test@git+https://github.com/dapphub/ds-test.git", "ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" uid e282159d5170298eb2455a6c05280ab5a73a4ef0 - resolved "https://github.com/dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0" + resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" "ds-test@https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" @@ -1457,9 +1453,9 @@ fast-diff@^1.1.2, fast-diff@^1.2.0: integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== fast-glob@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -1570,14 +1566,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +"forge-std@git+https://github.com/foundry-rs/forge-std.git": + version "1.7.1" + resolved "git+https://github.com/foundry-rs/forge-std.git#37a37ab73364d6644bfe11edf88a07880f99bd56" + "forge-std@git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e": version "1.5.6" resolved "git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e" -"forge-std@https://github.com/foundry-rs/forge-std": - version "1.7.1" - resolved "https://github.com/foundry-rs/forge-std#267acd30a625086b3f16e1a28cfe0c5097fa46b8" - "forge-std@https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421": version "1.7.1" resolved "https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421" @@ -2858,9 +2854,9 @@ progress@^2.0.0: integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== q@^1.5.1: version "1.5.1" @@ -3787,9 +3783,9 @@ universalify@^0.1.0: integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== uri-js@^4.2.2: version "4.4.1" @@ -3916,9 +3912,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^2.2.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" - integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" From 5cf76caeacc4a44bd7a1d4ec0b81e320482f6f36 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Fri, 10 Nov 2023 18:03:53 +0400 Subject: [PATCH 19/53] docs: remove an obsolete function --- .../content/modules/dispute/root_verification_module.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/src/content/modules/dispute/root_verification_module.md b/docs/src/content/modules/dispute/root_verification_module.md index 6bb6e1f0..b2ad1935 100644 --- a/docs/src/content/modules/dispute/root_verification_module.md +++ b/docs/src/content/modules/dispute/root_verification_module.md @@ -10,10 +10,9 @@ The Root Verification Module is a pre-dispute module that allows disputers to ca ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: Returns the decoded data for a request. -- `disputeResponse(bytes32 _requestId, bytes32 _responseId, address _disputer, address _proposer)`: Calculates the correct root 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 root 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`: Calculates the correct root 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 root as a response and finalizing the request. ### Request Parameters From 76613788cf16ff03697d08338771ef8e1db6f0c4 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Fri, 10 Nov 2023 18:04:48 +0400 Subject: [PATCH 20/53] Revert "docs: remove an obsolete function" This reverts commit 5cf76caeacc4a44bd7a1d4ec0b81e320482f6f36. --- .../content/modules/dispute/root_verification_module.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/src/content/modules/dispute/root_verification_module.md b/docs/src/content/modules/dispute/root_verification_module.md index b2ad1935..6bb6e1f0 100644 --- a/docs/src/content/modules/dispute/root_verification_module.md +++ b/docs/src/content/modules/dispute/root_verification_module.md @@ -10,9 +10,10 @@ The Root Verification Module is a pre-dispute module that allows disputers to ca ### Key Methods -- `decodeRequestData`: Returns the decoded data for a request. -- `disputeResponse`: Calculates the correct root 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 root as a response and finalizing the request. +- `decodeRequestData(bytes32 _requestId)`: Returns the decoded data for a request. +- `disputeResponse(bytes32 _requestId, bytes32 _responseId, address _disputer, address _proposer)`: Calculates the correct root 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 root 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. ### Request Parameters From 6e4d470fc3f41173de3b89b8f9251f2683be9e60 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Fri, 10 Nov 2023 15:57:38 +0400 Subject: [PATCH 21/53] feat: update `prophet-core` package --- package.json | 2 +- yarn.lock | 78 +++++++++++++++++++++++++--------------------------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index b7700bc1..dcb5ecad 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "package.json": "sort-package-json" }, "dependencies": { - "@defi-wonderland/prophet-core-contracts": "0.0.0-d05a00d0", + "@defi-wonderland/prophet-core-contracts": "0.0.0-a1d2cc55", "@defi-wonderland/solidity-utils": "0.0.0-3e9c8e8b", "@openzeppelin/contracts": "^4.9.3", "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", diff --git a/yarn.lock b/yarn.lock index c61b7709..658b1212 100644 --- a/yarn.lock +++ b/yarn.lock @@ -192,10 +192,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@defi-wonderland/prophet-core-contracts@0.0.0-d05a00d0": - version "0.0.0-d05a00d0" - resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-d05a00d0.tgz#1357d917fe46a5a12faa67f557e990255dda14fd" - integrity sha512-F/y0r/qDLFACzsN7Y2VRAPIS9Yhx2btU/m7cQT7T84TbIxAmBGVw6/7nb+HeIbXh+QDO90RP6vHAdQOow/q1Xw== +"@defi-wonderland/prophet-core-contracts@0.0.0-a1d2cc55": + version "0.0.0-a1d2cc55" + resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-a1d2cc55.tgz#e0bba63cdb143ffba6721049d2b0577eb39329fb" + integrity sha512-gl+8QvkzPd144yESzXhl2ceJ5blZczKh7HLioSfJ1uZnrJAR90z/MJD1fCb/1Q3oDyaq6WMWnJSK9VvMkadTtQ== dependencies: "@defi-wonderland/solidity-utils" "0.0.0-3e9c8e8b" "@openzeppelin/contracts" "^4.9.3" @@ -274,9 +274,9 @@ antlr4ts "^0.5.0-alpha.4" "@solidity-parser/parser@^0.16.0": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.1.tgz#f7c8a686974e1536da0105466c4db6727311253c" - integrity sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw== + version "0.16.2" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.2.tgz#42cb1e3d88b3e8029b0c9befff00b634cd92d2fa" + integrity sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg== dependencies: antlr4ts "^0.5.0-alpha.4" @@ -324,9 +324,9 @@ ts-essentials "^7.0.1" "@types/minimist@^1.2.0": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.4.tgz#81f886786411c45bba3f33e781ab48bd56bfca2e" - integrity sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@20.5.1": version "20.5.1" @@ -334,9 +334,9 @@ integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== "@types/normalize-package-data@^2.4.0": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz#291c243e4b94dbfbc0c0ee26b7666f1d5c030e2c" - integrity sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/prettier@^2.1.1": version "2.7.3" @@ -357,9 +357,9 @@ acorn-jsx@^5.0.0: integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== acorn@^6.0.7: version "6.4.2" @@ -367,9 +367,9 @@ acorn@^6.0.7: integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^8.4.1: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== add-stream@^1.0.0: version "1.0.0" @@ -1181,14 +1181,10 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -"ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": - version "1.0.0" - resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" - -"ds-test@https://github.com/dapphub/ds-test": +"ds-test@git+https://github.com/dapphub/ds-test.git", "ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" uid e282159d5170298eb2455a6c05280ab5a73a4ef0 - resolved "https://github.com/dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0" + resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" "ds-test@https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" @@ -1457,9 +1453,9 @@ fast-diff@^1.1.2, fast-diff@^1.2.0: integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== fast-glob@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -1570,14 +1566,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +"forge-std@git+https://github.com/foundry-rs/forge-std.git": + version "1.7.1" + resolved "git+https://github.com/foundry-rs/forge-std.git#37a37ab73364d6644bfe11edf88a07880f99bd56" + "forge-std@git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e": version "1.5.6" resolved "git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e" -"forge-std@https://github.com/foundry-rs/forge-std": - version "1.7.1" - resolved "https://github.com/foundry-rs/forge-std#267acd30a625086b3f16e1a28cfe0c5097fa46b8" - "forge-std@https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421": version "1.7.1" resolved "https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421" @@ -2858,9 +2854,9 @@ progress@^2.0.0: integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== q@^1.5.1: version "1.5.1" @@ -3787,9 +3783,9 @@ universalify@^0.1.0: integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== uri-js@^4.2.2: version "4.4.1" @@ -3916,9 +3912,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^2.2.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" - integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" From 1cdc837cac916da10426ed66b2dacdd6856afc8e Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Fri, 10 Nov 2023 19:09:33 +0400 Subject: [PATCH 22/53] feat: tidy up helpers --- solidity/test/utils/Helpers.sol | 70 ++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index af32e439..4f22491b 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -6,37 +6,53 @@ import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPl import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; +import {TestConstants} from './TestConstants.sol'; + +contract Helpers is DSTestPlus, TestConstants { + // 100% random sequence of bytes representing request, response, or dispute id + bytes32 public mockId = bytes32('69'); + + // Placeholder addresses + address public disputer = makeAddr('disputer'); + address public proposer = makeAddr('proposer'); + + // Mocks objects + IOracle.Request public mockRequest; + IOracle.Response public mockResponse = IOracle.Response({proposer: proposer, requestId: mockId, response: bytes('')}); + IOracle.Dispute public mockDispute = + IOracle.Dispute({disputer: disputer, responseId: mockId, proposer: proposer, requestId: mockId}); + + // Shared events that all modules emit + event RequestFinalized(bytes32 indexed _requestId, IOracle.Response _response, address _finalizer); -contract Helpers is DSTestPlus { modifier assumeFuzzable(address _address) { _assumeFuzzable(_address); _; } + /** + * @notice Ensures that a fuzzed address can be used for deployment and calls + * + * @param _address The address to check + */ function _assumeFuzzable(address _address) internal pure { assumeNotForgeAddress(_address); assumeNotZeroAddress(_address); assumeNotPrecompile(_address); } + /** + * @notice Sets up a mock and expects a call to it + * + * @param _receiver The address to have a mock on + * @param _calldata The calldata to mock and expect + * @param _returned The data to return from the mocked call + */ function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { vm.mockCall(_receiver, _calldata, _returned); vm.expectCall(_receiver, _calldata); } - function _getMockDispute( - bytes32 _requestId, - address _disputer, - address _proposer - ) internal view returns (IOracle.Dispute memory _dispute) { - _dispute = IOracle.Dispute({ - disputer: _disputer, - responseId: bytes32('response'), - proposer: _proposer, - requestId: _requestId - }); - } - function _forBondDepositERC20( IAccountingExtension _accountingExtension, address _depositor, @@ -52,14 +68,32 @@ contract Helpers is DSTestPlus { vm.stopPrank(); } - function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { - _id = keccak256(abi.encode(_response)); - } - + /** + * @notice Computes the ID of a given request as it's done in the Oracle + * + * @param _request The request to compute the ID for + * @return _id The ID of the request + */ function _getId(IOracle.Request memory _request) internal pure returns (bytes32 _id) { _id = keccak256(abi.encode(_request)); } + /** + * @notice Computes the ID of a given response as it's done in the Oracle + * + * @param _response The response to compute the ID for + * @return _id The ID of the response + */ + function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_response)); + } + + /** + * @notice Computes the ID of a given dispute as it's done in the Oracle + * + * @param _dispute The dispute to compute the ID for + * @return _id The ID of the dispute + */ function _getId(IOracle.Dispute memory _dispute) internal pure returns (bytes32 _id) { _id = keccak256(abi.encode(_dispute)); } From 2190a64a8c3a9ac1cb72367b2b53415c65346b47 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 14:34:04 +0400 Subject: [PATCH 23/53] docs: natspec --- .../request/contract_call_request_module.md | 2 +- .../request/IContractCallRequestModule.sol | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) 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 84e3f616..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,7 +10,7 @@ The `ContractCallRequestModule` is a module for requesting on-chain information. ### Key Methods -- `decodeRequestData(bytes calldata _data)`: 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. +- `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/interfaces/modules/request/IContractCallRequestModule.sol b/solidity/interfaces/modules/request/IContractCallRequestModule.sol index c68dbff1..e680fbeb 100644 --- a/solidity/interfaces/modules/request/IContractCallRequestModule.sol +++ b/solidity/interfaces/modules/request/IContractCallRequestModule.sol @@ -33,20 +33,27 @@ interface IContractCallRequestModule is IRequestModule { /** * @notice Returns the decoded data for a request - * @param _data The encoded request parameters - * @return _params The struct containing the parameters for the 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); - /// @inheritdoc IRequestModule + /** + * @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 + * @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, From 204ec3b3aeafc8337520a081cdf8ef31fbb10b02 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 14:34:27 +0400 Subject: [PATCH 24/53] test: fix `ContractCallRequestModule` unit tests --- .../request/ContractCallRequestModule.t.sol | 93 ++++++++++--------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/solidity/test/unit/modules/request/ContractCallRequestModule.t.sol b/solidity/test/unit/modules/request/ContractCallRequestModule.t.sol index 5e15640d..fa3e3ebe 100644 --- a/solidity/test/unit/modules/request/ContractCallRequestModule.t.sol +++ b/solidity/test/unit/modules/request/ContractCallRequestModule.t.sol @@ -36,12 +36,6 @@ contract BaseTest is Test, Helpers { address internal _targetContract = address(_token); bytes4 internal _functionSelector = bytes4(abi.encodeWithSignature('allowance(address,address)')); bytes internal _dataParams = abi.encode(_user, _user2); - bytes32 public mockId = bytes32('69'); - address internal _proposer = makeAddr('proposer'); - // Create a new dummy response - IOracle.Response public mockResponse; - - event RequestFinalized(bytes32 indexed _requestId, address _finalizer); /** * @notice Deploy the target and mock oracle+accounting extension @@ -54,8 +48,6 @@ contract BaseTest is Test, Helpers { vm.etch(address(accounting), hex'069420'); contractCallRequestModule = new ContractCallRequestModule(oracle); - - mockResponse = IOracle.Response({proposer: _proposer, requestId: mockId, response: bytes('')}); } } @@ -70,7 +62,7 @@ contract ContractCallRequestModule_Unit_ModuleData is BaseTest { /** * @notice Test that the decodeRequestData function returns the correct values */ - function test_decodeRequestData(bytes32 _requestId, IERC20 _paymentToken, uint256 _paymentAmount) public { + function test_decodeRequestData(IERC20 _paymentToken, uint256 _paymentAmount) public { bytes memory _requestData = abi.encode( IContractCallRequestModule.RequestParameters({ target: _targetContract, @@ -104,16 +96,8 @@ contract ContractCallRequestModule_Unit_FinalizeRequest is BaseTest { * - accounting extension pay * - accounting extension release */ - function test_makesCalls( - bytes32 _requestId, - address _requester, - address _proposer, - IERC20 _paymentToken, - uint256 _paymentAmount, - IOracle.Request calldata _request - ) public { - // Use the correct accounting parameters - bytes memory _requestData = abi.encode( + function test_finalizeWithResponse(IERC20 _paymentToken, uint256 _paymentAmount) public { + mockRequest.requestModuleData = abi.encode( IContractCallRequestModule.RequestParameters({ target: _targetContract, functionSelector: _functionSelector, @@ -124,36 +108,60 @@ contract ContractCallRequestModule_Unit_FinalizeRequest is BaseTest { }) ); + 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, _requester, _proposer, _paymentToken, _paymentAmount)), + abi.encodeCall( + IAccountingExtension.pay, + (_requestId, mockRequest.requester, mockResponse.proposer, _paymentToken, _paymentAmount) + ), abi.encode() ); vm.startPrank(address(oracle)); - contractCallRequestModule.finalizeRequest(_request, mockResponse, 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, (_requester, _requestId, _paymentToken, _paymentAmount)), + abi.encodeCall(IAccountingExtension.release, (mockRequest.requester, _requestId, _paymentToken, _paymentAmount)), abi.encode(true) ); - contractCallRequestModule.finalizeRequest(_request, mockResponse, address(this)); + vm.startPrank(address(oracle)); + contractCallRequestModule.finalizeRequest(mockRequest, mockResponse, address(oracle)); } - function test_emitsEvent( - bytes32 _requestId, - address _requester, - address _proposer, - IERC20 _paymentToken, - uint256 _paymentAmount, - IOracle.Request calldata _request - ) public { + function test_emitsEvent(IERC20 _paymentToken, uint256 _paymentAmount) public { // Use the correct accounting parameters - bytes memory _requestData = abi.encode( + mockRequest.requestModuleData = abi.encode( IContractCallRequestModule.RequestParameters({ target: _targetContract, functionSelector: _functionSelector, @@ -164,34 +172,31 @@ contract ContractCallRequestModule_Unit_FinalizeRequest is BaseTest { }) ); - // Mock and expect IAccountingExtension.pay to be called - _mockAndExpect( - address(accounting), - abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _paymentToken, _paymentAmount)), - abi.encode() - ); + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; - vm.startPrank(address(oracle)); - contractCallRequestModule.finalizeRequest(_request, mockResponse, address(oracle)); + // 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, (_requester, _requestId, _paymentToken, _paymentAmount)), + 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, address(this)); + emit RequestFinalized(_requestId, mockResponse, address(this)); - contractCallRequestModule.finalizeRequest(_request, 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(bytes32 _requestId, address _caller, IOracle.Request calldata _request) public { + 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? From 16f8ed13458ed84064f51bee7b8a3284ba23d548 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Fri, 10 Nov 2023 15:57:38 +0400 Subject: [PATCH 25/53] feat: update `prophet-core` package --- package.json | 2 +- yarn.lock | 84 +++++++++++++++++++++++++--------------------------- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index b7700bc1..dcb5ecad 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "package.json": "sort-package-json" }, "dependencies": { - "@defi-wonderland/prophet-core-contracts": "0.0.0-d05a00d0", + "@defi-wonderland/prophet-core-contracts": "0.0.0-a1d2cc55", "@defi-wonderland/solidity-utils": "0.0.0-3e9c8e8b", "@openzeppelin/contracts": "^4.9.3", "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", diff --git a/yarn.lock b/yarn.lock index c61b7709..f3d82dcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -192,10 +192,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@defi-wonderland/prophet-core-contracts@0.0.0-d05a00d0": - version "0.0.0-d05a00d0" - resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-d05a00d0.tgz#1357d917fe46a5a12faa67f557e990255dda14fd" - integrity sha512-F/y0r/qDLFACzsN7Y2VRAPIS9Yhx2btU/m7cQT7T84TbIxAmBGVw6/7nb+HeIbXh+QDO90RP6vHAdQOow/q1Xw== +"@defi-wonderland/prophet-core-contracts@0.0.0-a1d2cc55": + version "0.0.0-a1d2cc55" + resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-a1d2cc55.tgz#e0bba63cdb143ffba6721049d2b0577eb39329fb" + integrity sha512-gl+8QvkzPd144yESzXhl2ceJ5blZczKh7HLioSfJ1uZnrJAR90z/MJD1fCb/1Q3oDyaq6WMWnJSK9VvMkadTtQ== dependencies: "@defi-wonderland/solidity-utils" "0.0.0-3e9c8e8b" "@openzeppelin/contracts" "^4.9.3" @@ -274,9 +274,9 @@ antlr4ts "^0.5.0-alpha.4" "@solidity-parser/parser@^0.16.0": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.1.tgz#f7c8a686974e1536da0105466c4db6727311253c" - integrity sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw== + version "0.16.2" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.2.tgz#42cb1e3d88b3e8029b0c9befff00b634cd92d2fa" + integrity sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg== dependencies: antlr4ts "^0.5.0-alpha.4" @@ -324,9 +324,9 @@ ts-essentials "^7.0.1" "@types/minimist@^1.2.0": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.4.tgz#81f886786411c45bba3f33e781ab48bd56bfca2e" - integrity sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@20.5.1": version "20.5.1" @@ -334,9 +334,9 @@ integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== "@types/normalize-package-data@^2.4.0": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz#291c243e4b94dbfbc0c0ee26b7666f1d5c030e2c" - integrity sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/prettier@^2.1.1": version "2.7.3" @@ -357,9 +357,9 @@ acorn-jsx@^5.0.0: integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== acorn@^6.0.7: version "6.4.2" @@ -367,9 +367,9 @@ acorn@^6.0.7: integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^8.4.1: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== add-stream@^1.0.0: version "1.0.0" @@ -1181,14 +1181,10 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -"ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": - version "1.0.0" - resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" - -"ds-test@https://github.com/dapphub/ds-test": +"ds-test@git+https://github.com/dapphub/ds-test.git", "ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" uid e282159d5170298eb2455a6c05280ab5a73a4ef0 - resolved "https://github.com/dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0" + resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" "ds-test@https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" @@ -1457,9 +1453,9 @@ fast-diff@^1.1.2, fast-diff@^1.2.0: integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== fast-glob@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -1570,14 +1566,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +"forge-std@git+https://github.com/foundry-rs/forge-std.git": + version "1.7.1" + resolved "git+https://github.com/foundry-rs/forge-std.git#37a37ab73364d6644bfe11edf88a07880f99bd56" + "forge-std@git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e": version "1.5.6" resolved "git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e" -"forge-std@https://github.com/foundry-rs/forge-std": - version "1.7.1" - resolved "https://github.com/foundry-rs/forge-std#267acd30a625086b3f16e1a28cfe0c5097fa46b8" - "forge-std@https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421": version "1.7.1" resolved "https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421" @@ -2858,9 +2854,9 @@ progress@^2.0.0: integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== q@^1.5.1: version "1.5.1" @@ -3265,9 +3261,9 @@ solhint@3.5.1: prettier "^2.8.3" solidity-ast@^0.4.38: - version "0.4.52" - resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.52.tgz#9f1a9abc7e5ba28bbf91146ecd07aec7e70f3c85" - integrity sha512-iOya9BSiB9jhM8Vf40n8lGELGzwrUc57rl5BhfNtJ5cvAaMvRcNlHeAMNvqJJyjoUnczqRbHqdivEqK89du3Cw== + version "0.4.53" + resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.53.tgz#098259472fccd234ff00f050afaf7843a7ccd635" + integrity sha512-/7xYF//mAt4iP9S21fCFSLMouYXzXJqrd84jbI1LHL1rq7XhyFLUXxVcRkl9KqEEQmI+DmDbTeS6W5cEwcJ2wQ== dependencies: array.prototype.findlast "^1.2.2" @@ -3787,9 +3783,9 @@ universalify@^0.1.0: integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== uri-js@^4.2.2: version "4.4.1" @@ -3916,9 +3912,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^2.2.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" - integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" From 3c2813359555007e1e74bf7a0cb73774952f1d51 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 14:37:26 +0400 Subject: [PATCH 26/53] feat: update helpers --- solidity/test/utils/Helpers.sol | 70 ++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index af32e439..4f22491b 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -6,37 +6,53 @@ import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPl import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; +import {TestConstants} from './TestConstants.sol'; + +contract Helpers is DSTestPlus, TestConstants { + // 100% random sequence of bytes representing request, response, or dispute id + bytes32 public mockId = bytes32('69'); + + // Placeholder addresses + address public disputer = makeAddr('disputer'); + address public proposer = makeAddr('proposer'); + + // Mocks objects + IOracle.Request public mockRequest; + IOracle.Response public mockResponse = IOracle.Response({proposer: proposer, requestId: mockId, response: bytes('')}); + IOracle.Dispute public mockDispute = + IOracle.Dispute({disputer: disputer, responseId: mockId, proposer: proposer, requestId: mockId}); + + // Shared events that all modules emit + event RequestFinalized(bytes32 indexed _requestId, IOracle.Response _response, address _finalizer); -contract Helpers is DSTestPlus { modifier assumeFuzzable(address _address) { _assumeFuzzable(_address); _; } + /** + * @notice Ensures that a fuzzed address can be used for deployment and calls + * + * @param _address The address to check + */ function _assumeFuzzable(address _address) internal pure { assumeNotForgeAddress(_address); assumeNotZeroAddress(_address); assumeNotPrecompile(_address); } + /** + * @notice Sets up a mock and expects a call to it + * + * @param _receiver The address to have a mock on + * @param _calldata The calldata to mock and expect + * @param _returned The data to return from the mocked call + */ function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { vm.mockCall(_receiver, _calldata, _returned); vm.expectCall(_receiver, _calldata); } - function _getMockDispute( - bytes32 _requestId, - address _disputer, - address _proposer - ) internal view returns (IOracle.Dispute memory _dispute) { - _dispute = IOracle.Dispute({ - disputer: _disputer, - responseId: bytes32('response'), - proposer: _proposer, - requestId: _requestId - }); - } - function _forBondDepositERC20( IAccountingExtension _accountingExtension, address _depositor, @@ -52,14 +68,32 @@ contract Helpers is DSTestPlus { vm.stopPrank(); } - function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { - _id = keccak256(abi.encode(_response)); - } - + /** + * @notice Computes the ID of a given request as it's done in the Oracle + * + * @param _request The request to compute the ID for + * @return _id The ID of the request + */ function _getId(IOracle.Request memory _request) internal pure returns (bytes32 _id) { _id = keccak256(abi.encode(_request)); } + /** + * @notice Computes the ID of a given response as it's done in the Oracle + * + * @param _response The response to compute the ID for + * @return _id The ID of the response + */ + function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_response)); + } + + /** + * @notice Computes the ID of a given dispute as it's done in the Oracle + * + * @param _dispute The dispute to compute the ID for + * @return _id The ID of the dispute + */ function _getId(IOracle.Dispute memory _dispute) internal pure returns (bytes32 _id) { _id = keccak256(abi.encode(_dispute)); } From 826470634eeec373e7d075206d4d9cbf83346d2d Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:28:57 +0400 Subject: [PATCH 27/53] test: fix unit tests --- .../modules/request/HttpRequestModule.t.sol | 122 ++++++++++-------- 1 file changed, 68 insertions(+), 54 deletions(-) diff --git a/solidity/test/unit/modules/request/HttpRequestModule.t.sol b/solidity/test/unit/modules/request/HttpRequestModule.t.sol index e7e300e7..a7baf406 100644 --- a/solidity/test/unit/modules/request/HttpRequestModule.t.sol +++ b/solidity/test/unit/modules/request/HttpRequestModule.t.sol @@ -18,23 +18,15 @@ import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountin */ contract BaseTest is Test, Helpers { // Mock request data - string public constant URL = 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd'; + // Fuzzing enums doesn't work: https://github.com/foundry-rs/foundry/issues/871 IHttpRequestModule.HttpMethod public constant METHOD = IHttpRequestModule.HttpMethod.GET; - string public constant BODY = '69420'; - // Mock token - IERC20 public immutable TOKEN = IERC20(makeAddr('ERC20')); // The target contract HttpRequestModule public httpRequestModule; // A mock oracle IOracle public oracle; // A mock accounting extension - IAccountingExtension public accounting = IAccountingExtension(makeAddr('accounting')); - IOracle.Response public mockResponse; - address internal _proposer = makeAddr('proposer'); - bytes32 public mockId = bytes32('69'); - - event RequestFinalized(bytes32 indexed _requestId, address _finalizer); + IAccountingExtension public accounting; /** * @notice Deploy the target and mock oracle+accounting extension @@ -47,7 +39,6 @@ contract BaseTest is Test, Helpers { vm.etch(address(accounting), hex'069420'); httpRequestModule = new HttpRequestModule(oracle); - mockResponse = IOracle.Response({proposer: _proposer, requestId: mockId, response: bytes('')}); } } @@ -62,12 +53,12 @@ contract HttpRequestModule_Unit_ModuleData is BaseTest { /** * @notice Test that the decodeRequestData function returns the correct values */ - function test_decodeRequestData(bytes32 _requestId, uint256 _amount, IERC20 _token) public { + function test_decodeRequestData(string memory _url, string memory _body, uint256 _amount, IERC20 _token) public { bytes memory _requestData = abi.encode( IHttpRequestModule.RequestParameters({ - url: URL, + url: _url, + body: _body, method: METHOD, - body: BODY, accountingExtension: accounting, paymentToken: _token, paymentAmount: _amount @@ -78,9 +69,9 @@ contract HttpRequestModule_Unit_ModuleData is BaseTest { IHttpRequestModule.RequestParameters memory _params = httpRequestModule.decodeRequestData(_requestData); // Check: decoded values match original values? - assertEq(_params.url, URL); + assertEq(_params.url, _url); assertEq(uint256(_params.method), uint256(METHOD)); - assertEq(_params.body, BODY); + assertEq(_params.body, _body); assertEq(address(_params.accountingExtension), address(accounting)); assertEq(address(_params.paymentToken), address(_token)); assertEq(_params.paymentAmount, _amount); @@ -95,77 +86,99 @@ contract HttpRequestModule_Unit_FinalizeRequest is BaseTest { * - accounting extension pay * - accounting extension release */ - function test_makesCalls( - bytes32 _requestId, - address _requester, - address _proposer, + function test_finalizeWithResponse( + string calldata _url, + string calldata _body, uint256 _amount, - IERC20 _token, - IOracle.Request calldata _request + IERC20 _token ) public { _amount = bound(_amount, 0, type(uint248).max); // Use the correct accounting parameters - bytes memory _requestData = abi.encode( + mockRequest.requestModuleData = abi.encode( IHttpRequestModule.RequestParameters({ - url: URL, + url: _url, method: METHOD, - body: BODY, + body: _body, accountingExtension: accounting, paymentToken: _token, paymentAmount: _amount }) ); - // Mock and expect IAccountingExtension.pay to be called + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + + // Mock and expect oracle to return the response's creation time _mockAndExpect( - address(accounting), - abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _token, _amount)), - abi.encode() + address(oracle), abi.encodeCall(IOracle.createdAt, (_getId(mockResponse))), abi.encode(block.timestamp) ); - vm.startPrank(address(oracle)); - httpRequestModule.finalizeRequest(_request, mockResponse, address(oracle)); - - // Mock and expect IAccountingExtension.release to be called + // Mock and expect IAccountingExtension.pay to be called _mockAndExpect( address(accounting), - abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _token, _amount)), - abi.encode(true) + abi.encodeCall( + IAccountingExtension.pay, (_requestId, mockRequest.requester, mockResponse.proposer, _token, _amount) + ), + abi.encode() ); - httpRequestModule.finalizeRequest(_request, mockResponse, address(this)); + vm.startPrank(address(oracle)); + httpRequestModule.finalizeRequest(mockRequest, mockResponse, address(this)); } - function test_emitsEvent( - bytes32 _requestId, - address _requester, - address _proposer, + function test_finalizeWithoutResponse( + string calldata _url, + string calldata _body, uint256 _amount, - IERC20 _token, - IOracle.Request calldata _request + IERC20 _token ) public { + _amount = bound(_amount, 0, type(uint248).max); + // Use the correct accounting parameters - bytes memory _requestData = abi.encode( + mockRequest.requestModuleData = abi.encode( IHttpRequestModule.RequestParameters({ - url: URL, + url: _url, method: METHOD, - body: BODY, + body: _body, accountingExtension: accounting, paymentToken: _token, paymentAmount: _amount }) ); - // Mock and expect IAccountingExtension.pay to be called + 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.pay, (_requestId, _requester, _proposer, _token, _amount)), - abi.encode() + abi.encodeCall(IAccountingExtension.release, (mockRequest.requester, _requestId, _token, _amount)), + abi.encode(true) ); vm.startPrank(address(oracle)); - httpRequestModule.finalizeRequest(_request, mockResponse, address(oracle)); + httpRequestModule.finalizeRequest(mockRequest, mockResponse, address(this)); + } + + function test_emitsEvent(string calldata _url, string calldata _body, uint256 _amount, IERC20 _token) public { + // Use the correct accounting parameters + mockRequest.requestModuleData = abi.encode( + IHttpRequestModule.RequestParameters({ + url: _url, + method: METHOD, + body: _body, + accountingExtension: accounting, + paymentToken: _token, + paymentAmount: _amount + }) + ); + + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; // Update mock call to return the response's createdAt _mockAndExpect(address(oracle), abi.encodeCall(IOracle.createdAt, (_getId(mockResponse))), abi.encode(0)); @@ -173,27 +186,28 @@ contract HttpRequestModule_Unit_FinalizeRequest is BaseTest { // Mock and expect IAccountingExtension.release to be called _mockAndExpect( address(accounting), - abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _token, _amount)), + abi.encodeCall(IAccountingExtension.release, (mockRequest.requester, _requestId, _token, _amount)), abi.encode(true) ); // Check: is the event emitted? vm.expectEmit(true, true, true, true, address(httpRequestModule)); - emit RequestFinalized(_requestId, address(this)); + emit RequestFinalized(_requestId, mockResponse, address(this)); - httpRequestModule.finalizeRequest(_request, mockResponse, address(this)); + vm.prank(address(oracle)); + httpRequestModule.finalizeRequest(mockRequest, mockResponse, address(this)); } /** * @notice Test that the finalizeRequest reverts if caller is not the oracle */ - function test_revertsIfWrongCaller(bytes32 _requestId, address _caller, IOracle.Request calldata _request) public { + function test_revertsIfWrongCaller(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); - httpRequestModule.finalizeRequest(_request, mockResponse, address(_caller)); + httpRequestModule.finalizeRequest(mockRequest, mockResponse, address(_caller)); } } From ae2e40a7bbe3d5f2c385145ad0060e3c99842078 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:30:44 +0400 Subject: [PATCH 28/53] docs: clarify docs and natspec --- .../modules/request/http_request_module.md | 2 +- .../modules/request/IHttpRequestModule.sol | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/src/content/modules/request/http_request_module.md b/docs/src/content/modules/request/http_request_module.md index 332e16d5..faf5de94 100644 --- a/docs/src/content/modules/request/http_request_module.md +++ b/docs/src/content/modules/request/http_request_module.md @@ -10,7 +10,7 @@ The `HttpRequestModule` is a contract that allows users to request HTTP calls. ### Key Methods -- `decodeRequestData(bytes calldata _data)`: This method decodes the data for a request given its ID. It returns the URL, HTTP method, body, accounting extension, payment token, and payment amount associated with the request. +- `decodeRequestData`: This method decodes request parameters. It returns the URL, HTTP method, body, accounting extension, payment token, and payment amount from the given data. - `finalizeRequest`: This method finalizes a request by paying the proposer if there is a valid response, or releases the requester bond if no valid response was provided. ### Request Parameters diff --git a/solidity/interfaces/modules/request/IHttpRequestModule.sol b/solidity/interfaces/modules/request/IHttpRequestModule.sol index 9a01a797..a4e3db93 100644 --- a/solidity/interfaces/modules/request/IHttpRequestModule.sol +++ b/solidity/interfaces/modules/request/IHttpRequestModule.sol @@ -52,20 +52,27 @@ interface IHttpRequestModule is IRequestModule { /** * @notice Returns the decoded data for a request - * @param _data The encoded request parameters - * @return _params The struct containing the parameters for the 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); - /// @inheritdoc IRequestModule + /** + * @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 + * @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, From 008420483bf123254879822d0ce6bb09e654bac9 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:30:51 +0400 Subject: [PATCH 29/53] fix: onlyOracle modifier --- solidity/contracts/modules/request/HttpRequestModule.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/contracts/modules/request/HttpRequestModule.sol b/solidity/contracts/modules/request/HttpRequestModule.sol index ad5324fc..78531a18 100644 --- a/solidity/contracts/modules/request/HttpRequestModule.sol +++ b/solidity/contracts/modules/request/HttpRequestModule.sol @@ -21,7 +21,7 @@ contract HttpRequestModule is Module, IHttpRequestModule { } /// @inheritdoc IHttpRequestModule - function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external { + function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external onlyOracle { RequestParameters memory _params = decodeRequestData(_data); _params.accountingExtension.bond({ From 1ebbb639f589663dcaab64b2b88bfaf53164a03d Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:44:44 +0400 Subject: [PATCH 30/53] fix: onlyOracle modifier --- .../contracts/modules/request/ContractCallRequestModule.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/contracts/modules/request/ContractCallRequestModule.sol b/solidity/contracts/modules/request/ContractCallRequestModule.sol index 578e552a..311487ec 100644 --- a/solidity/contracts/modules/request/ContractCallRequestModule.sol +++ b/solidity/contracts/modules/request/ContractCallRequestModule.sol @@ -21,7 +21,7 @@ contract ContractCallRequestModule is Module, IContractCallRequestModule { } /// @inheritdoc IContractCallRequestModule - function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external { + function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external onlyOracle { RequestParameters memory _params = decodeRequestData(_data); _params.accountingExtension.bond({ From c82e4764a500871a4ae711d3abc20928e5d6db77 Mon Sep 17 00:00:00 2001 From: Gas <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:45:04 +0400 Subject: [PATCH 31/53] perf: optimize `BondedResponseModule` (#3) --- .../response/bonded_response_module.md | 13 +- package.json | 2 +- .../modules/response/BondedResponseModule.sol | 209 ++--- .../extensions/IAccountingExtension.sol | 380 ++++----- .../response/IBondedResponseModule.sol | 193 ++--- .../test/integration/ResponseProposal.t.sol | 36 - .../response/BondedResponseModule.t.sol | 806 ++++++++---------- solidity/test/utils/Helpers.sol | 157 ++-- yarn.lock | 58 +- 9 files changed, 851 insertions(+), 1003 deletions(-) diff --git a/docs/src/content/modules/response/bonded_response_module.md b/docs/src/content/modules/response/bonded_response_module.md index b8c98800..4aaa6c67 100644 --- a/docs/src/content/modules/response/bonded_response_module.md +++ b/docs/src/content/modules/response/bonded_response_module.md @@ -10,10 +10,9 @@ The Bonded Response Module is a contract that allows users to propose a response ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: Returns the decoded data for a request. -- `propose(bytes32 _requestId, address _proposer, bytes calldata _responseData)`: Proposes a response for a request, bonding the proposer's tokens. -- `deleteResponse(bytes32 _requestId, bytes32 _responseId, address _proposer)`: Allows a user to delete an undisputed response they proposed before the deadline, releasing the bond. -- `finalizeRequest(bytes32 _requestId, address _finalizer)`: Finalizes the request. +- `decodeRequestData`: Returns the decoded data for a request. +- `propose`: Proposes a response for a request, bonding the proposer's tokens. +- `finalizeRequest`: Finalizes the request. ### Request Parameters @@ -24,14 +23,12 @@ The Bonded Response Module is a contract that allows users to propose a response ## 3. Key Mechanisms & Concepts -- Deleting a response: If a proposer realizes the response they've submitted is incorrect, they can delete it. Note that disputed responses cannot be taken back. - - Early finalization: It is possible for pre-dispute modules to atomically calculate the correct response on-chain, decide on the result of a dispute and finalize the request before its deadline. +- Dispute window: Prevents proposers from submitting a response 1 block before the deadline and finalizing it in the next block, leaving disputers no time to dispute the response. + ## 4. Gotchas - In case of no valid responses, a request can be finalized after the deadline and the requester will get back their tokens. -- A proposer might submit a response 1 block before the deadline and finalize it in the next block, making it impossible to dispute. - Users cannot propose a response after the deadline for a request. - Users cannot propose a response if an undisputed response has already been proposed. -- Users cannot delete a response after the proposing deadline. diff --git a/package.json b/package.json index b7700bc1..dcb5ecad 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "package.json": "sort-package-json" }, "dependencies": { - "@defi-wonderland/prophet-core-contracts": "0.0.0-d05a00d0", + "@defi-wonderland/prophet-core-contracts": "0.0.0-a1d2cc55", "@defi-wonderland/solidity-utils": "0.0.0-3e9c8e8b", "@openzeppelin/contracts": "^4.9.3", "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", diff --git a/solidity/contracts/modules/response/BondedResponseModule.sol b/solidity/contracts/modules/response/BondedResponseModule.sol index 3dffd2fb..c52bf5df 100644 --- a/solidity/contracts/modules/response/BondedResponseModule.sol +++ b/solidity/contracts/modules/response/BondedResponseModule.sol @@ -1,118 +1,91 @@ -// // 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 {IBondedResponseModule} from '../../../interfaces/modules/response/IBondedResponseModule.sol'; - -// contract BondedResponseModule is Module, IBondedResponseModule { -// constructor(IOracle _oracle) Module(_oracle) {} - -// /// @inheritdoc IModule -// function moduleName() public pure returns (string memory _moduleName) { -// _moduleName = 'BondedResponseModule'; -// } - -// /// @inheritdoc IBondedResponseModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } - -// /// @inheritdoc IBondedResponseModule -// function propose( -// bytes32 _requestId, -// address _proposer, -// bytes calldata _responseData, -// address _sender -// ) external onlyOracle returns (IOracle.Response memory _response) { -// RequestParameters memory _params = decodeRequestData(_requestId); - -// // Cannot propose after the deadline -// if (block.timestamp >= _params.deadline) revert BondedResponseModule_TooLateToPropose(); - -// // Cannot propose to a request with a response, unless the response is being disputed -// bytes32[] memory _responseIds = ORACLE.getResponseIds(_requestId); -// uint256 _responsesLength = _responseIds.length; - -// if (_responsesLength != 0) { -// bytes32 _disputeId = ORACLE.getResponse(_responseIds[_responsesLength - 1]).disputeId; - -// // Allowing one undisputed response at a time -// if (_disputeId == bytes32(0)) revert BondedResponseModule_AlreadyResponded(); -// IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId); -// // TODO: leaving a note here to re-check this check if a new status is added -// // If the dispute was lost, we assume the proposed answer was correct. DisputeStatus.None should not be reachable due to the previous check. -// if (_dispute.status == IOracle.DisputeStatus.Lost) revert BondedResponseModule_AlreadyResponded(); -// } - -// _response = IOracle.Response({ -// requestId: _requestId, -// disputeId: bytes32(0), -// proposer: _proposer, -// response: _responseData, -// createdAt: block.timestamp -// }); - -// _params.accountingExtension.bond({ -// _bonder: _response.proposer, -// _requestId: _requestId, -// _token: _params.bondToken, -// _amount: _params.bondSize, -// _sender: _sender -// }); - -// emit ProposeResponse(_requestId, _proposer, _responseData); -// } - -// /// @inheritdoc IBondedResponseModule -// function deleteResponse(bytes32 _requestId, bytes32, address _proposer) external onlyOracle { -// RequestParameters memory _params = decodeRequestData(_requestId); - -// if (block.timestamp > _params.deadline) revert BondedResponseModule_TooLateToDelete(); - -// _params.accountingExtension.release({ -// _bonder: _proposer, -// _requestId: _requestId, -// _token: _params.bondToken, -// _amount: _params.bondSize -// }); -// } - -// /// @inheritdoc IBondedResponseModule -// function finalizeRequest( -// bytes32 _requestId, -// address _finalizer -// ) external override(IBondedResponseModule, Module) onlyOracle { -// RequestParameters memory _params = decodeRequestData(_requestId); - -// bool _isModule = ORACLE.allowedModule(_requestId, _finalizer); - -// if (!_isModule && block.timestamp < _params.deadline) { -// revert BondedResponseModule_TooEarlyToFinalize(); -// } - -// IOracle.Response memory _response = ORACLE.getFinalizedResponse(_requestId); -// if (_response.createdAt != 0) { -// if (!_isModule && block.timestamp < _response.createdAt + _params.disputeWindow) { -// revert BondedResponseModule_TooEarlyToFinalize(); -// } - -// _params.accountingExtension.release({ -// _bonder: _response.proposer, -// _requestId: _requestId, -// _token: _params.bondToken, -// _amount: _params.bondSize -// }); -// } -// emit RequestFinalized(_requestId, _finalizer); -// } - -// /// @inheritdoc Module -// function _afterSetupRequest(bytes32, bytes calldata _data) internal view override { -// RequestParameters memory _params = abi.decode(_data, (RequestParameters)); -// if (_params.deadline <= block.timestamp) { -// revert BondedResponseModule_InvalidRequest(); -// } -// } -// } +// 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 {IBondedResponseModule} from '../../../interfaces/modules/response/IBondedResponseModule.sol'; + +contract BondedResponseModule is Module, IBondedResponseModule { + constructor(IOracle _oracle) Module(_oracle) {} + + /// @inheritdoc IModule + function moduleName() public pure returns (string memory _moduleName) { + _moduleName = 'BondedResponseModule'; + } + + /// @inheritdoc IBondedResponseModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } + + /// @inheritdoc IBondedResponseModule + function propose( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _sender + ) external onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.responseModuleData); + + // Cannot propose after the deadline + if (block.timestamp >= _params.deadline) revert BondedResponseModule_TooLateToPropose(); + + // Cannot propose to a request with a response, unless the response is being disputed + bytes32[] memory _responseIds = ORACLE.getResponseIds(_response.requestId); + uint256 _responsesLength = _responseIds.length; + + if (_responsesLength != 0) { + bytes32 _disputeId = ORACLE.disputeOf(_responseIds[_responsesLength - 1]); + + // Allowing one undisputed response at a time + if (_disputeId == bytes32(0)) revert BondedResponseModule_AlreadyResponded(); + IOracle.DisputeStatus _status = ORACLE.disputeStatus(_disputeId); + // TODO: leaving a note here to re-check this check if a new status is added + // If the dispute was lost, we assume the proposed answer was correct. DisputeStatus.None should not be reachable due to the previous check. + if (_status == IOracle.DisputeStatus.Lost) revert BondedResponseModule_AlreadyResponded(); + } + + _params.accountingExtension.bond({ + _bonder: _response.proposer, + _requestId: _response.requestId, + _token: _params.bondToken, + _amount: _params.bondSize, + _sender: _sender + }); + + emit ResponseProposed(_response.requestId, _response, block.number); + } + + /// @inheritdoc IBondedResponseModule + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external override(IBondedResponseModule, Module) onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.responseModuleData); + + // TODO: If deadline has passed, we can skip the caller validation + bool _isModule = ORACLE.allowedModule(_response.requestId, _finalizer); + + if (!_isModule && block.timestamp < _params.deadline) { + revert BondedResponseModule_TooEarlyToFinalize(); + } + + uint256 _responseCreatedAt = ORACLE.createdAt(_getId(_response)); + + if (_responseCreatedAt != 0) { + if (!_isModule && block.timestamp < _responseCreatedAt + _params.disputeWindow) { + revert BondedResponseModule_TooEarlyToFinalize(); + } + + _params.accountingExtension.release({ + _bonder: _response.proposer, + _requestId: _response.requestId, + _token: _params.bondToken, + _amount: _params.bondSize + }); + } + + emit RequestFinalized(_response.requestId, _response, _finalizer); + } +} diff --git a/solidity/interfaces/extensions/IAccountingExtension.sol b/solidity/interfaces/extensions/IAccountingExtension.sol index 5a42243e..74c9c9cd 100644 --- a/solidity/interfaces/extensions/IAccountingExtension.sol +++ b/solidity/interfaces/extensions/IAccountingExtension.sol @@ -1,190 +1,190 @@ -// // 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'; - -// /* -// * @title AccountingExtension -// * @notice Extension allowing users to deposit and bond funds -// * to be used for payments and disputes. -// */ -// interface IAccountingExtension { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice A user deposited tokens into the accounting extension -// * @param _depositor The user who deposited the tokens -// * @param _token The address of the token deposited by the user -// * @param _amount The amount of `_token` deposited -// */ -// event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A user withdrew tokens from the accounting extension -// * @param _withdrawer The user who withdrew the tokens -// * @param _token The address of the token withdrawn by the user -// * @param _amount The amount of `_token` withdrawn -// */ -// event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A payment between users has been made -// * @param _beneficiary The user receiving the tokens -// * @param _payer The user who is getting its tokens transferred -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` transferred -// */ -// event Paid( -// bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount -// ); - -// /** -// * @notice User's funds have been bonded -// * @param _bonder The user who is getting its tokens bonded -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` bonded -// */ -// event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice User's funds have been released -// * @param _beneficiary The user who is getting its tokens released -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` released -// */ -// event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Thrown when the account doesn't have enough balance to bond/withdraw -// * or not enough bonded to release/pay -// */ -// error AccountingExtension_InsufficientFunds(); - -// /** -// * @notice Thrown when the module bonding user tokens hasn't been approved by the user. -// */ -// error AccountingExtension_InsufficientAllowance(); - -// /** -// * @notice Thrown when an `onlyAllowedModule` function is called by something -// * else than a module being used in the corresponding request -// */ -// error AccountingExtension_UnauthorizedModule(); - -// /** -// * @notice Thrown when an `onlyParticipant` function is called with an address -// * that is not part of the request. -// */ -// error AccountingExtension_UnauthorizedUser(); - -// /*/////////////////////////////////////////////////////////////// -// VARIABLES -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Returns the interface for the Oracle contract -// */ -// function ORACLE() external view returns (IOracle _oracle); - -// /** -// * @notice Returns the amount of a token a user has bonded -// * @param _user The address of the user with bonded tokens -// * @param _bondToken The token bonded -// * @param _requestId The id of the request the user bonded for -// * @return _amount The amount of `_bondToken` bonded -// */ -// function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); - -// /** -// * @notice Returns the amount of a token a user has deposited -// * @param _user The address of the user with deposited tokens -// * @param _token The token deposited -// * @return _amount The amount of `_token` deposited -// */ -// function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Transfers tokens from a user and updates his virtual balance -// * @dev The user must have approved the accounting extension to transfer the tokens. -// * @param _token The address of the token being deposited -// * @param _amount The amount of `_token` to deposit -// */ -// function deposit(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows an user to withdraw deposited tokens -// * @param _token The address of the token being withdrawn -// * @param _amount The amount of `_token` to withdraw -// */ -// function withdraw(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to transfer bonded tokens from one user to another -// * @dev Only the virtual balances in the accounting extension are modified. The token contract -// * is not called nor its balances modified. -// * @param _requestId The id of the request handling the user's tokens -// * @param _payer The address of the user paying the tokens -// * @param _receiver The address of the user receiving the tokens -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` being transferred -// */ -// function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a valid module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// * @param _sender The address starting the propose call on the Oracle -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; - -// /** -// * @notice Allows a valid module to release a user's tokens -// * @param _bonder The address of the user to release tokens for -// * @param _requestId The id of the request where the tokens were bonded -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` to release -// */ -// function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a user to approve a module for bonding tokens -// * @param _module The address of the module to be approved -// */ -// function approveModule(address _module) external; - -// /** -// * @notice Allows a user to revoke a module's approval for bonding tokens -// * @param _module The address of the module to be revoked -// */ -// function revokeModule(address _module) external; - -// /** -// * @notice Returns a list of all modules a user has approved -// * @param _user The address of the user -// * @return _approvedModules The array of all modules approved by the user -// */ -// function approvedModules(address _user) external view returns (address[] memory _approvedModules); -// } +// 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'; + +/* + * @title AccountingExtension + * @notice Extension allowing users to deposit and bond funds + * to be used for payments and disputes. + */ +interface IAccountingExtension { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A user deposited tokens into the accounting extension + * @param _depositor The user who deposited the tokens + * @param _token The address of the token deposited by the user + * @param _amount The amount of `_token` deposited + */ + event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A user withdrew tokens from the accounting extension + * @param _withdrawer The user who withdrew the tokens + * @param _token The address of the token withdrawn by the user + * @param _amount The amount of `_token` withdrawn + */ + event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A payment between users has been made + * @param _beneficiary The user receiving the tokens + * @param _payer The user who is getting its tokens transferred + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` transferred + */ + event Paid( + bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount + ); + + /** + * @notice User's funds have been bonded + * @param _bonder The user who is getting its tokens bonded + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` bonded + */ + event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); + + /** + * @notice User's funds have been released + * @param _beneficiary The user who is getting its tokens released + * @param _token The address of the token being released + * @param _amount The amount of `_token` released + */ + event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Thrown when the account doesn't have enough balance to bond/withdraw + * or not enough bonded to release/pay + */ + error AccountingExtension_InsufficientFunds(); + + /** + * @notice Thrown when the module bonding user tokens hasn't been approved by the user. + */ + error AccountingExtension_InsufficientAllowance(); + + /** + * @notice Thrown when an `onlyAllowedModule` function is called by something + * else than a module being used in the corresponding request + */ + error AccountingExtension_UnauthorizedModule(); + + /** + * @notice Thrown when an `onlyParticipant` function is called with an address + * that is not part of the request. + */ + error AccountingExtension_UnauthorizedUser(); + + /*/////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the interface for the Oracle contract + */ + function ORACLE() external view returns (IOracle _oracle); + + /** + * @notice Returns the amount of a token a user has bonded + * @param _user The address of the user with bonded tokens + * @param _bondToken The token bonded + * @param _requestId The id of the request the user bonded for + * @return _amount The amount of `_bondToken` bonded + */ + function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); + + /** + * @notice Returns the amount of a token a user has deposited + * @param _user The address of the user with deposited tokens + * @param _token The token deposited + * @return _amount The amount of `_token` deposited + */ + function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Transfers tokens from a user and updates his virtual balance + * @dev The user must have approved the accounting extension to transfer the tokens. + * @param _token The address of the token being deposited + * @param _amount The amount of `_token` to deposit + */ + function deposit(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows an user to withdraw deposited tokens + * @param _token The address of the token being withdrawn + * @param _amount The amount of `_token` to withdraw + */ + function withdraw(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to transfer bonded tokens from one user to another + * @dev Only the virtual balances in the accounting extension are modified. The token contract + * is not called nor its balances modified. + * @param _requestId The id of the request handling the user's tokens + * @param _payer The address of the user paying the tokens + * @param _receiver The address of the user receiving the tokens + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` being transferred + */ + function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a valid module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + * @param _sender The address starting the propose call on the Oracle + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; + + /** + * @notice Allows a valid module to release a user's tokens + * @param _bonder The address of the user to release tokens for + * @param _requestId The id of the request where the tokens were bonded + * @param _token The address of the token being released + * @param _amount The amount of `_token` to release + */ + function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a user to approve a module for bonding tokens + * @param _module The address of the module to be approved + */ + function approveModule(address _module) external; + + /** + * @notice Allows a user to revoke a module's approval for bonding tokens + * @param _module The address of the module to be revoked + */ + function revokeModule(address _module) external; + + /** + * @notice Returns a list of all modules a user has approved + * @param _user The address of the user + * @return _approvedModules The array of all modules approved by the user + */ + function approvedModules(address _user) external view returns (address[] memory _approvedModules); +} diff --git a/solidity/interfaces/modules/response/IBondedResponseModule.sol b/solidity/interfaces/modules/response/IBondedResponseModule.sol index c159b988..1b460221 100644 --- a/solidity/interfaces/modules/response/IBondedResponseModule.sol +++ b/solidity/interfaces/modules/response/IBondedResponseModule.sol @@ -1,117 +1,102 @@ -// // 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 {IResponseModule} from -// '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/response/IResponseModule.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IResponseModule} from + '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/response/IResponseModule.sol'; -// import {IAccountingExtension} from '../../extensions/IAccountingExtension.sol'; +import {IAccountingExtension} from '../../extensions/IAccountingExtension.sol'; -// /* -// * @title BondedResponseModule -// * @notice Module allowing users to propose a response for a request -// * by bonding tokens. -// */ -// interface IBondedResponseModule is IResponseModule { -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Emitted when a response is proposed -// * @param _requestId The ID of the request that the response was proposed -// * @param _proposer The user that proposed the response -// * @param _responseData The data for the response -// */ -// event ProposeResponse(bytes32 indexed _requestId, address _proposer, bytes _responseData); -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ +/* + * @title BondedResponseModule + * @notice Module allowing users to propose a response for a request by bonding tokens + */ +interface IBondedResponseModule is IResponseModule { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + /** + * @notice Emitted when a response is proposed + * + * @param _requestId The ID of the request that the response was proposed + * @param _response The proposed response + * @param _blockNumber The number of the block in which the response was proposed + */ + event ResponseProposed(bytes32 indexed _requestId, IOracle.Response _response, uint256 indexed _blockNumber); -// /** -// * @notice Thrown when trying to finalize a request before the deadline -// */ -// error BondedResponseModule_TooEarlyToFinalize(); + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Thrown when trying to propose a response after deadline -// */ -// error BondedResponseModule_TooLateToPropose(); + /** + * @notice Thrown when trying to finalize a request before the deadline + */ + error BondedResponseModule_TooEarlyToFinalize(); -// /** -// * @notice Thrown when trying to propose a response while an undisputed response is already proposed -// */ -// error BondedResponseModule_AlreadyResponded(); + /** + * @notice Thrown when trying to propose a response after deadline + */ + error BondedResponseModule_TooLateToPropose(); -// /** -// * @notice Thrown when trying to delete a response after the proposing deadline -// */ -// error BondedResponseModule_TooLateToDelete(); + /** + * @notice Thrown when trying to propose a response while an undisputed response is already proposed + */ + error BondedResponseModule_AlreadyResponded(); -// /** -// * @notice Thrown when trying to create an invalid request -// */ -// error BondedResponseModule_InvalidRequest(); + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ + /** + * @notice Parameters of the request as stored in the module + * + * @param accountingExtension The accounting extension used to bond and release tokens + * @param bondToken The token used for bonds in the request + * @param bondSize The amount of `_bondToken` to bond to propose a response and dispute + * @param deadline The timestamp after which no responses can be proposed + * @param disputeWindow The time buffer required to finalize a request + */ + struct RequestParameters { + IAccountingExtension accountingExtension; + IERC20 bondToken; + uint256 bondSize; + uint256 deadline; + uint256 disputeWindow; + } -// /** -// * @notice Parameters of the request as stored in the module -// * @param accountingExtension The accounting extension used to bond and release tokens -// * @param bondToken The token used for bonds in the request -// * @param bondSize The amount of `_bondToken` to bond to propose a response and dispute -// * @param deadline The timestamp after which no responses can be proposed -// * @param disputeWindow The time buffer required to finalize a request -// */ -// struct RequestParameters { -// IAccountingExtension accountingExtension; -// IERC20 bondToken; -// uint256 bondSize; -// uint256 deadline; -// uint256 disputeWindow; -// } + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the decoded data for a request + * + * @param _data The encoded data + * @return _params The struct containing the parameters for the request + */ + function decodeRequestData(bytes calldata _data) external pure returns (RequestParameters memory _params); -// /** -// * @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 Proposes a response for a request, bonding the proposer's tokens + * + * @dev The user must have previously deposited tokens into the accounting extension + * @param _request The request to propose a response to + * @param _response The response being proposed + * @param _sender The address that initiated the transaction + */ + function propose(IOracle.Request calldata _request, IOracle.Response calldata _response, address _sender) external; -// /** -// * @notice Proposes a response for a request, bonding the proposer's tokens -// * @dev The user must have previously deposited tokens into the accounting extension -// * @param _requestId The ID of the request to propose a response for -// * @param _proposer The user proposing the response -// * @param _responseData The data for the response -// * @param _sender The address calling propose on the Oracle -// * @return _response The struct of proposed response -// */ -// function propose( -// bytes32 _requestId, -// address _proposer, -// bytes calldata _responseData, -// address _sender -// ) external returns (IOracle.Response memory _response); - -// /** -// * @notice Allows a user to delete an undisputed response they proposed before the deadline, releasing the bond -// * @param _requestId The ID of the request to delete the response from -// * @param _responseId The ID of the response to delete -// * @param _proposer The user who proposed the response -// */ -// function deleteResponse(bytes32 _requestId, bytes32 _responseId, address _proposer) external; - -// /** -// * @notice Finalizes the request by releasing the bond of the proposer -// * @param _requestId The ID of the request to finalize -// * @param _finalizer The user who triggered the finalization -// */ -// function finalizeRequest(bytes32 _requestId, address _finalizer) external; -// } + /** + * @notice Finalizes the request by releasing the bond of the proposer + * + * @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/integration/ResponseProposal.t.sol b/solidity/test/integration/ResponseProposal.t.sol index d7809076..a17486d7 100644 --- a/solidity/test/integration/ResponseProposal.t.sol +++ b/solidity/test/integration/ResponseProposal.t.sol @@ -149,40 +149,4 @@ // assertEq(_deletedResponse.createdAt, 0); // assertEq(_deletedResponse.disputeId, bytes32(0)); // } - -// function test_deleteResponse_afterDeadline(bytes memory _responseData, uint256 _timestamp) public { -// vm.assume(_timestamp > _expectedDeadline); - -// _forBondDepositERC20(_accountingExtension, proposer, usdc, _expectedBondSize, _expectedBondSize); - -// vm.startPrank(proposer); -// _accountingExtension.approveModule(address(_responseModule)); -// bytes32 _responseId = oracle.proposeResponse(_requestId, _responseData); -// vm.stopPrank(); - -// vm.warp(_timestamp); - -// vm.expectRevert(IBondedResponseModule.BondedResponseModule_TooLateToDelete.selector); - -// vm.prank(proposer); -// oracle.deleteResponse(_responseId); -// } - -// function test_proposeResponse_finalizedRequest(bytes memory _responseData, uint256 _timestamp) public { -// vm.assume(_timestamp > _expectedDeadline + _baseDisputeWindow); - -// _forBondDepositERC20(_accountingExtension, proposer, usdc, _expectedBondSize, _expectedBondSize); - -// vm.startPrank(proposer); -// _accountingExtension.approveModule(address(_responseModule)); -// bytes32 _responseId = oracle.proposeResponse(_requestId, _responseData); -// vm.stopPrank(); - -// vm.warp(_timestamp); -// oracle.finalize(_requestId, _responseId); - -// vm.expectRevert(abi.encodeWithSelector(IOracle.Oracle_AlreadyFinalized.selector, _requestId)); -// vm.prank(proposer); -// oracle.proposeResponse(_requestId, _responseData); -// } // } diff --git a/solidity/test/unit/modules/response/BondedResponseModule.t.sol b/solidity/test/unit/modules/response/BondedResponseModule.t.sol index 9d16f581..30c2133c 100644 --- a/solidity/test/unit/modules/response/BondedResponseModule.t.sol +++ b/solidity/test/unit/modules/response/BondedResponseModule.t.sol @@ -1,461 +1,345 @@ -// // 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 { -// BondedResponseModule, -// IBondedResponseModule, -// IModule, -// IOracle -// } from '../../../../contracts/modules/response/BondedResponseModule.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_BondedResponseModule is BondedResponseModule { -// constructor(IOracle _oracle) BondedResponseModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } -// } - -// /** -// * @title Bonded Response Module Unit tests -// */ -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_BondedResponseModule public bondedResponseModule; -// // A mock oracle -// IOracle public oracle; -// // A mock accounting extension -// IAccountingExtension public accounting = IAccountingExtension(makeAddr('accounting')); -// // Base dispute window -// uint256 internal _baseDisputeWindow = 12 hours; - -// event ProposeResponse(bytes32 indexed _requestId, address _proposer, bytes _responseData); -// 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'); - -// // vm.etch(address(token), hex'069420'); - -// // Avoid starting at 0 for time sensitive tests -// vm.warp(123_456); - -// bondedResponseModule = new ForTest_BondedResponseModule(oracle); -// } -// } - -// contract BondedResponseModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(bondedResponseModule.moduleName(), 'BondedResponseModule'); -// } - -// /** -// * @notice Test that the decodeRequestData function returns the correct values -// */ -// function test_decodeRequestData( -// bytes32 _requestId, -// uint256 _bondSize, -// uint256 _deadline, -// uint256 _disputeWindow, -// IERC20 _token -// ) public { -// // Create and set some mock request data -// bytes memory _data = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); -// bondedResponseModule.forTest_setRequestData(_requestId, _data); - -// // Get the returned values -// IBondedResponseModule.RequestParameters memory _params = bondedResponseModule.decodeRequestData(_requestId); - -// // Check: correct values returned? -// assertEq(address(_params.accountingExtension), address(accounting), 'Mismatch: accounting extension address'); -// assertEq(address(_params.bondToken), address(_token), 'Mismatch: token address'); -// assertEq(_params.bondSize, _bondSize, 'Mismatch: bond size'); -// assertEq(_params.deadline, _deadline, 'Mismatch: deadline'); -// assertEq(_params.disputeWindow, _disputeWindow, 'Mismatch: dispute window'); -// } -// } - -// contract BondedResponseModule_Unit_Setup is BaseTest { -// function test_setupRequestRevertsIfInvalidRequest( -// IAccountingExtension _accounting, -// IERC20 _token, -// uint256 _deadline, -// uint256 _bondSize -// ) public { -// _deadline = bound(_deadline, 0, block.timestamp); - -// IBondedResponseModule.RequestParameters memory _requestParams = IBondedResponseModule.RequestParameters({ -// accountingExtension: _accounting, -// bondToken: _token, -// bondSize: _bondSize, -// deadline: _deadline, -// disputeWindow: _baseDisputeWindow -// }); - -// // Check: does it revert if the provided deadline is invalid? -// vm.expectRevert(IBondedResponseModule.BondedResponseModule_InvalidRequest.selector); -// vm.prank(address(oracle)); - -// bondedResponseModule.setupRequest(bytes32(0), abi.encode(_requestParams)); -// } -// } - -// contract BondedResponseModule_Unit_Propose is BaseTest { -// /** -// * @notice Test that the propose function is only callable by the oracle -// */ -// function test_revertIfNotOracle( -// bytes32 _requestId, -// address _sender, -// address _proposer, -// bytes calldata _responseData -// ) public { -// vm.assume(_sender != address(oracle)); - -// // Check: does it revert if not called by the Oracle? -// vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); - -// vm.prank(address(_sender)); -// bondedResponseModule.propose(_requestId, _proposer, _responseData, _sender); -// } - -// /** -// * @notice Test that the propose function works correctly and triggers _afterPropose (which bonds) -// */ -// function test_propose( -// bytes32 _requestId, -// uint256 _bondSize, -// uint256 _deadline, -// uint256 _disputeWindow, -// bytes calldata _responseData, -// address _sender, -// IERC20 _token, -// address _proposer -// ) public { -// _deadline = bound(_deadline, block.timestamp + 1, type(uint248).max); -// _disputeWindow = bound(_disputeWindow, 61, 365 days); -// _bondSize = bound(_bondSize, 0, type(uint248).max); - -// // Create and set some mock request data -// bytes memory _data = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); -// bondedResponseModule.forTest_setRequestData(_requestId, _data); - -// // Mock and expect IOracle.getResponseIds to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getResponseIds, _requestId), abi.encode(new bytes32[](0))); - -// // Mock and expect IAccountingExtension.bond to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeWithSignature( -// 'bond(address,bytes32,address,uint256,address)', _proposer, _requestId, _token, _bondSize, _sender -// ), -// abi.encode() -// ); - -// vm.prank(address(oracle)); -// IOracle.Response memory _responseReturned = -// bondedResponseModule.propose(_requestId, _proposer, _responseData, _sender); - -// IOracle.Response memory _responseExpected = IOracle.Response({ -// createdAt: block.timestamp, -// requestId: _requestId, -// disputeId: bytes32(''), -// proposer: _proposer, -// response: _responseData -// }); - -// // Check: correct response struct returned? -// assertEq(_responseReturned.requestId, _responseExpected.requestId, 'Mismatch: request ID'); -// assertEq(_responseReturned.disputeId, _responseExpected.disputeId, 'Mismatch: dispute ID'); -// assertEq(_responseReturned.proposer, _responseExpected.proposer, 'Mismatch: proposer address'); -// assertEq(_responseReturned.response, _responseExpected.response, 'Mismatch: response object'); -// } - -// function test_emitsEvent( -// bytes32 _requestId, -// uint256 _bondSize, -// uint256 _deadline, -// uint256 _disputeWindow, -// bytes calldata _responseData, -// address _sender, -// IERC20 _token, -// address _proposer -// ) public { -// _deadline = bound(_deadline, block.timestamp + 1, type(uint248).max); -// _disputeWindow = bound(_disputeWindow, 61, 365 days); - -// // Create and set some mock request data -// bytes memory _data = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); -// bondedResponseModule.forTest_setRequestData(_requestId, _data); - -// // Mock and expect IOracle.getResponseIds to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getResponseIds, _requestId), abi.encode(new bytes32[](0))); - -// // Mock and expect IOracle.getResponseIds to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeWithSignature( -// 'bond(address,bytes32,address,uint256,address)', _proposer, _requestId, _token, _bondSize, _sender -// ), -// abi.encode() -// ); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(bondedResponseModule)); -// emit ProposeResponse(_requestId, _proposer, _responseData); - -// vm.prank(address(oracle)); -// bondedResponseModule.propose(_requestId, _proposer, _responseData, _sender); -// } -// } - -// contract BondedResponseModule_Unit_FinalizeRequest is BaseTest { -// /** -// * @notice Test that the propose function is only callable by the oracle -// */ -// function test_calls( -// bytes32 _requestId, -// uint256 _bondSize, -// uint256 _deadline, -// uint256 _disputeWindow, -// IERC20 _token, -// address _proposer -// ) public { -// _deadline = bound(_deadline, block.timestamp + 1, type(uint248).max); -// _disputeWindow = bound(_disputeWindow, 61, 365 days); - -// // Check revert if deadline has not passed -// bytes memory _data = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); -// bondedResponseModule.forTest_setRequestData(_requestId, _data); - -// // Mock and expect IOracle.allowedModule to be called -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.allowedModule, (_requestId, address(this))), abi.encode(false) -// ); - -// // Check: does it revert if it's too early to finalize? -// vm.expectRevert(IBondedResponseModule.BondedResponseModule_TooEarlyToFinalize.selector); - -// vm.prank(address(oracle)); -// bondedResponseModule.finalizeRequest(_requestId, address(this)); - -// // Check correct calls are made if deadline has passed -// _deadline = block.timestamp; - -// _data = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); -// bondedResponseModule.forTest_setRequestData(_requestId, _data); - -// IOracle.Response memory _mockResponse = IOracle.Response({ -// createdAt: block.timestamp, -// requestId: _requestId, -// disputeId: bytes32(''), -// proposer: _proposer, -// response: bytes('bleh') -// }); - -// // Mock and expect IOracle.getFinalizedResponse to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, _requestId), abi.encode(_mockResponse)); - -// // Mock and expect IAccountingExtension.release to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.release, (_proposer, _requestId, _token, _bondSize)), -// abi.encode(true) -// ); - -// vm.warp(block.timestamp + _disputeWindow); - -// vm.prank(address(oracle)); -// bondedResponseModule.finalizeRequest(_requestId, address(this)); -// } - -// function test_emitsEvent( -// bytes32 _requestId, -// uint256 _bondSize, -// uint256 _deadline, -// uint256 _disputeWindow, -// IERC20 _token, -// address _proposer -// ) public { -// _deadline = bound(_deadline, block.timestamp + 1, type(uint248).max); -// _disputeWindow = bound(_disputeWindow, 61, 365 days); - -// // Check revert if deadline has not passed -// bytes memory _data = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); -// bondedResponseModule.forTest_setRequestData(_requestId, _data); - -// // Mock and expect IOracle.allowedModule to be called -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.allowedModule, (_requestId, address(this))), abi.encode(false) -// ); - -// // Check correct calls are made if deadline has passed -// _deadline = block.timestamp; - -// _data = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); -// bondedResponseModule.forTest_setRequestData(_requestId, _data); - -// IOracle.Response memory _mockResponse = IOracle.Response({ -// createdAt: block.timestamp, -// requestId: _requestId, -// disputeId: bytes32(''), -// proposer: _proposer, -// response: bytes('bleh') -// }); - -// // Mock and expect IOracle.getFinalizedResponse to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, _requestId), abi.encode(_mockResponse)); - -// // Mock and expect IAccountingExtension.release to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.release, (_proposer, _requestId, _token, _bondSize)), -// abi.encode(true) -// ); - -// // Check: is event emitted? -// vm.expectEmit(true, true, true, true, address(bondedResponseModule)); -// emit RequestFinalized(_requestId, address(this)); - -// vm.warp(block.timestamp + _disputeWindow); - -// vm.prank(address(oracle)); -// bondedResponseModule.finalizeRequest(_requestId, address(this)); -// } - -// /** -// * @notice Test that the finalize function can be called by an allowed module before the time window. -// */ -// function test_earlyByModule( -// bytes32 _requestId, -// uint256 _bondSize, -// uint256 _deadline, -// IERC20 _token, -// address _proposer -// ) public { -// _deadline = bound(_deadline, block.timestamp + 1, type(uint248).max); - -// address _allowedModule = makeAddr('allowed module'); -// bytes memory _data = abi.encode(accounting, _token, _bondSize, _deadline, _baseDisputeWindow); -// bondedResponseModule.forTest_setRequestData(_requestId, _data); - -// // Mock and expect IOracle.allowedModule to be called -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.allowedModule, (_requestId, _allowedModule)), abi.encode(true) -// ); - -// IOracle.Response memory _mockResponse = IOracle.Response({ -// createdAt: block.timestamp, -// requestId: _requestId, -// disputeId: bytes32(''), -// proposer: _proposer, -// response: bytes('bleh') -// }); - -// // Mock and expect IOracle.getFinalizedResponse to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, _requestId), abi.encode(_mockResponse)); - -// // Mock and expect IAccountingExtension.release to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.release, (_proposer, _requestId, _token, _bondSize)), -// abi.encode(true) -// ); - -// vm.prank(address(oracle)); -// bondedResponseModule.finalizeRequest(_requestId, _allowedModule); -// } - -// /** -// * @notice Test that the finalizing a request during a response dispute window will revert. -// */ -// function test_revertDuringDisputeWindow( -// bytes32 _requestId, -// uint256 _bondSize, -// uint256 _deadline, -// IERC20 _token, -// address _proposer -// ) public { -// _deadline = bound(_deadline, block.timestamp + 1, type(uint248).max); - -// address _finalizer = makeAddr('finalizer'); -// bytes memory _data = abi.encode(accounting, _token, _bondSize, _deadline, _baseDisputeWindow); -// bondedResponseModule.forTest_setRequestData(_requestId, _data); - -// // Mock and expect IOracle.allowedModule to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.allowedModule, (_requestId, _finalizer)), abi.encode(false)); - -// IOracle.Response memory _mockResponse = IOracle.Response({ -// createdAt: _deadline - 1, -// requestId: _requestId, -// disputeId: bytes32(''), -// proposer: _proposer, -// response: bytes('bleh') -// }); - -// // Mock and expect IOracle.getFinalizedResponse to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, _requestId), abi.encode(_mockResponse)); - -// vm.expectRevert(IBondedResponseModule.BondedResponseModule_TooEarlyToFinalize.selector); - -// vm.warp(_deadline + 1); -// vm.prank(address(oracle)); -// bondedResponseModule.finalizeRequest(_requestId, _finalizer); -// } -// } - -// contract BondedResponseModule_Unit_DeleteResponse is BaseTest { -// /** -// * @notice Test that the delete response function triggers bond release. -// */ -// function test_deleteResponse( -// bytes32 _requestId, -// bytes32 _responseId, -// uint256 _bondSize, -// uint256 _deadline, -// uint256 _timestamp, -// IERC20 _token, -// address _proposer -// ) public { -// _timestamp = bound(_timestamp, 1, type(uint248).max); - -// // Create and set some mock request data -// bytes memory _data = abi.encode(accounting, _token, _bondSize, _deadline, _baseDisputeWindow); -// bondedResponseModule.forTest_setRequestData(_requestId, _data); - -// vm.warp(_timestamp); - -// if (_deadline >= _timestamp) { -// // Mock and expect IAccountingExtension.release to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.release, (_proposer, _requestId, _token, _bondSize)), -// abi.encode() -// ); -// } else { -// // Check: does it revert if the deadline has passed? -// vm.expectRevert(IBondedResponseModule.BondedResponseModule_TooLateToDelete.selector); -// } - -// vm.prank(address(oracle)); -// bondedResponseModule.deleteResponse(_requestId, _responseId, _proposer); -// } -// } +// 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 { + BondedResponseModule, + IBondedResponseModule, + IModule, + IOracle +} from '../../../../contracts/modules/response/BondedResponseModule.sol'; + +import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; + +/** + * @title Bonded Response Module Unit tests + */ +contract BaseTest is Test, Helpers { + // The target contract + BondedResponseModule public bondedResponseModule; + // A mock oracle + IOracle public oracle; + // A mock accounting extension + IAccountingExtension public accounting = IAccountingExtension(makeAddr('Accounting')); + // Base dispute window + uint256 internal _baseDisputeWindow = 12 hours; + + // Events + event ResponseProposed(bytes32 indexed _requestId, IOracle.Response _response, uint256 indexed _blockNumber); + + /** + * @notice Deploy the target and mock oracle+accounting extension + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + // Avoid starting at 0 for time sensitive tests + vm.warp(123_456); + + bondedResponseModule = new BondedResponseModule(oracle); + } +} + +contract BondedResponseModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(bondedResponseModule.moduleName(), 'BondedResponseModule'); + } + + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData(IERC20 _token, uint256 _bondSize, uint256 _deadline, uint256 _disputeWindow) public { + // Create and set some mock request data + bytes memory _data = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); + + // Get the returned values + IBondedResponseModule.RequestParameters memory _params = bondedResponseModule.decodeRequestData(_data); + + // Check: correct values returned? + assertEq(address(_params.accountingExtension), address(accounting), 'Mismatch: accounting extension address'); + assertEq(address(_params.bondToken), address(_token), 'Mismatch: token address'); + assertEq(_params.bondSize, _bondSize, 'Mismatch: bond size'); + assertEq(_params.deadline, _deadline, 'Mismatch: deadline'); + assertEq(_params.disputeWindow, _disputeWindow, 'Mismatch: dispute window'); + } +} + +contract BondedResponseModule_Unit_Propose is BaseTest { + /** + * @notice Test that the propose function is only callable by the oracle + */ + function test_revertIfNotOracle(address _sender) public { + vm.assume(_sender != address(oracle)); + + // Check: does it revert if not called by the Oracle? + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + + vm.prank(address(_sender)); + bondedResponseModule.propose(mockRequest, mockResponse, _sender); + } + + /** + * @notice Test that the propose function works correctly and bonds the proposer's funds + */ + function test_propose( + IERC20 _token, + uint256 _bondSize, + uint256 _deadline, + uint256 _disputeWindow, + address _sender, + address _proposer + ) public assumeFuzzable(_sender) assumeFuzzable(_proposer) { + _deadline = bound(_deadline, block.timestamp + 1, type(uint248).max); + _disputeWindow = bound(_disputeWindow, 61, 365 days); + _bondSize = bound(_bondSize, 0, type(uint248).max); + + // Set the response module parameters + mockRequest.responseModuleData = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); + + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + mockResponse.proposer = _proposer; + + // Mock and expect IOracle.getResponseIds to be called + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getResponseIds, _requestId), abi.encode(new bytes32[](0))); + + // Mock and expect IAccountingExtension.bond to be called + _mockAndExpect( + address(accounting), + abi.encodeWithSignature( + 'bond(address,bytes32,address,uint256,address)', _proposer, _requestId, _token, _bondSize, _sender + ), + abi.encode() + ); + + vm.prank(address(oracle)); + bondedResponseModule.propose(mockRequest, mockResponse, _sender); + } + + function test_emitsEvent( + IERC20 _token, + uint256 _bondSize, + uint256 _deadline, + uint256 _disputeWindow, + address _sender, + address _proposer + ) public { + _deadline = bound(_deadline, block.timestamp + 1, type(uint248).max); + _disputeWindow = bound(_disputeWindow, 61, 365 days); + + // Create and set some mock request data + mockRequest.responseModuleData = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + mockResponse.proposer = _proposer; + + // Mock and expect IOracle.getResponseIds to be called + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getResponseIds, _requestId), abi.encode(new bytes32[](0))); + + // Mock and expect IOracle.getResponseIds to be called + _mockAndExpect( + address(accounting), + abi.encodeWithSignature( + 'bond(address,bytes32,address,uint256,address)', _proposer, _requestId, _token, _bondSize, _sender + ), + abi.encode() + ); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(bondedResponseModule)); + emit ResponseProposed({_requestId: _requestId, _response: mockResponse, _blockNumber: block.number}); + + vm.prank(address(oracle)); + bondedResponseModule.propose(mockRequest, mockResponse, _sender); + } +} + +contract BondedResponseModule_Unit_FinalizeRequest is BaseTest { + /** + * @notice Test that the propose function is only callable by the oracle + */ + function test_revertIfNotOracle(address _sender) public { + vm.assume(_sender != address(oracle)); + + // Check: does it revert if not called by the Oracle? + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + + vm.prank(address(_sender)); + bondedResponseModule.finalizeRequest(mockRequest, mockResponse, _sender); + } + + function test_revertsBeforeDeadline( + IERC20 _token, + uint256 _bondSize, + uint256 _deadline, + uint256 _disputeWindow, + address _proposer + ) public { + _deadline = bound(_deadline, block.timestamp + 1, type(uint248).max); + _disputeWindow = bound(_disputeWindow, 61, 365 days); + + // Check revert if deadline has not passed + mockRequest.responseModuleData = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); + mockResponse.requestId = _getId(mockRequest); + mockResponse.proposer = _proposer; + + // Mock and expect IOracle.allowedModule to be called + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.allowedModule, (_getId(mockRequest), address(this))), abi.encode(false) + ); + + // Check: does it revert if it's too early to finalize? + vm.expectRevert(IBondedResponseModule.BondedResponseModule_TooEarlyToFinalize.selector); + + vm.prank(address(oracle)); + bondedResponseModule.finalizeRequest(mockRequest, mockResponse, address(this)); + } + + function test_releasesBond( + IERC20 _token, + uint256 _bondSize, + uint256 _deadline, + uint256 _disputeWindow, + address _proposer + ) public { + _disputeWindow = bound(_disputeWindow, 61, 365 days); + + // Check correct calls are made if deadline has passed + _deadline = block.timestamp; + mockRequest.responseModuleData = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); + mockResponse.requestId = _getId(mockRequest); + mockResponse.proposer = _proposer; + + // Mock and expect IOracle.allowedModule to be called + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.allowedModule, (_getId(mockRequest), address(this))), abi.encode(true) + ); + + // Mock and expect IOracle.createdAt to be called + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.createdAt, (_getId(mockResponse))), abi.encode(block.timestamp) + ); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (_proposer, _getId(mockRequest), _token, _bondSize)), + abi.encode(true) + ); + + vm.warp(block.timestamp + _disputeWindow); + + vm.prank(address(oracle)); + bondedResponseModule.finalizeRequest(mockRequest, mockResponse, address(this)); + } + + function test_emitsEvent(IERC20 _token, uint256 _bondSize, uint256 _disputeWindow, address _proposer) public { + _disputeWindow = bound(_disputeWindow, 61, 365 days); + + // Check correct calls are made if deadline has passed + uint256 _deadline = block.timestamp; + mockRequest.responseModuleData = abi.encode(accounting, _token, _bondSize, _deadline, _disputeWindow); + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + mockResponse.proposer = _proposer; + + // Mock and expect IOracle.allowedModule to be called + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.allowedModule, (_requestId, address(this))), abi.encode(false) + ); + + // Mock and expect IOracle.createdAt to be called + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.createdAt, (_getId(mockResponse))), abi.encode(block.timestamp) + ); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (_proposer, _getId(mockRequest), _token, _bondSize)), + abi.encode(true) + ); + + // Check: is event emitted? + vm.expectEmit(true, true, true, true, address(bondedResponseModule)); + emit RequestFinalized({_requestId: _getId(mockRequest), _response: mockResponse, _finalizer: address(this)}); + + vm.warp(block.timestamp + _disputeWindow); + + vm.prank(address(oracle)); + bondedResponseModule.finalizeRequest(mockRequest, mockResponse, address(this)); + } + + /** + * @notice Test that the finalize function can be called by an allowed module before the time window. + */ + function test_earlyByModule( + IERC20 _token, + uint256 _bondSize, + uint256 _deadline, + address _proposer, + address _allowedModule + ) public assumeFuzzable(_allowedModule) { + _deadline = bound(_deadline, block.timestamp + 1, type(uint248).max); + mockRequest.responseModuleData = abi.encode(accounting, _token, _bondSize, _deadline, _baseDisputeWindow); + + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + mockResponse.proposer = _proposer; + + // Mock and expect IOracle.allowedModule to be called + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.allowedModule, (_requestId, _allowedModule)), abi.encode(true) + ); + + // Mock and expect IOracle.createdAt to be called + _mockAndExpect( + address(oracle), + abi.encodeCall(IOracle.createdAt, (_getId(mockResponse))), + abi.encode(block.timestamp - _baseDisputeWindow) + ); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (_proposer, _requestId, _token, _bondSize)), + abi.encode(true) + ); + + vm.prank(address(oracle)); + bondedResponseModule.finalizeRequest(mockRequest, mockResponse, _allowedModule); + } + + /** + * @notice Test that the finalizing a request during a response dispute window will revert. + */ + function test_revertDuringDisputeWindow( + IERC20 _token, + uint256 _bondSize, + uint256 _deadline, + address _finalizer + ) public { + _deadline = bound(_deadline, block.timestamp + 1, type(uint248).max); + + mockRequest.responseModuleData = abi.encode(accounting, _token, _bondSize, _deadline, _baseDisputeWindow); + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + + // Mock and expect IOracle.allowedModule to be called + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.allowedModule, (_requestId, _finalizer)), abi.encode(false)); + + vm.expectRevert(IBondedResponseModule.BondedResponseModule_TooEarlyToFinalize.selector); + + vm.prank(address(oracle)); + bondedResponseModule.finalizeRequest(mockRequest, mockResponse, _finalizer); + } +} diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 46874489..213a0064 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -1,56 +1,101 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; - -// import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; - -// contract Helpers is DSTestPlus { -// modifier assumeFuzzable(address _address) { -// _assumeFuzzable(_address); -// _; -// } - -// function _assumeFuzzable(address _address) internal pure { -// assumeNotForgeAddress(_address); -// assumeNotZeroAddress(_address); -// assumeNotPrecompile(_address); -// } - -// function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { -// vm.mockCall(_receiver, _calldata, _returned); -// vm.expectCall(_receiver, _calldata); -// } - -// function _getMockDispute( -// bytes32 _requestId, -// address _disputer, -// address _proposer -// ) internal view returns (IOracle.Dispute memory _dispute) { -// _dispute = IOracle.Dispute({ -// disputer: _disputer, -// responseId: bytes32('response'), -// proposer: _proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.None, -// createdAt: block.timestamp -// }); -// } - -// function _forBondDepositERC20( -// IAccountingExtension _accountingExtension, -// address _depositor, -// IERC20 _token, -// uint256 _depositAmount, -// uint256 _balanceIncrease -// ) internal { -// vm.assume(_balanceIncrease >= _depositAmount); -// deal(address(_token), _depositor, _balanceIncrease); -// vm.startPrank(_depositor); -// _token.approve(address(_accountingExtension), _depositAmount); -// _accountingExtension.deposit(_token, _depositAmount); -// vm.stopPrank(); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; + +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; +import {TestConstants} from './TestConstants.sol'; + +contract Helpers is DSTestPlus, TestConstants { + // 100% random sequence of bytes representing request, response, or dispute id + bytes32 public mockId = bytes32('69'); + + // Placeholder addresses + address public disputer = makeAddr('disputer'); + address public proposer = makeAddr('proposer'); + + // Mocks objects + IOracle.Request public mockRequest; + IOracle.Response public mockResponse = IOracle.Response({proposer: proposer, requestId: mockId, response: bytes('')}); + IOracle.Dispute public mockDispute = + IOracle.Dispute({disputer: disputer, responseId: mockId, proposer: proposer, requestId: mockId}); + + // Shared events that all modules emit + event RequestFinalized(bytes32 indexed _requestId, IOracle.Response _response, address _finalizer); + + modifier assumeFuzzable(address _address) { + _assumeFuzzable(_address); + _; + } + + /** + * @notice Ensures that a fuzzed address can be used for deployment and calls + * + * @param _address The address to check + */ + function _assumeFuzzable(address _address) internal pure { + assumeNotForgeAddress(_address); + assumeNotZeroAddress(_address); + assumeNotPrecompile(_address); + } + + /** + * @notice Sets up a mock and expects a call to it + * + * @param _receiver The address to have a mock on + * @param _calldata The calldata to mock and expect + * @param _returned The data to return from the mocked call + */ + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + // TODO: What does _balanceIncrease do? + function _forBondDepositERC20( + IAccountingExtension _accountingExtension, + address _depositor, + IERC20 _token, + uint256 _depositAmount, + uint256 _balanceIncrease + ) internal { + vm.assume(_balanceIncrease >= _depositAmount); + deal(address(_token), _depositor, _balanceIncrease); + vm.startPrank(_depositor); + _token.approve(address(_accountingExtension), _depositAmount); + _accountingExtension.deposit(_token, _depositAmount); + vm.stopPrank(); + } + + /** + * @notice Computes the ID of a given request as it's done in the Oracle + * + * @param _request The request to compute the ID for + * @return _id The ID of the request + */ + function _getId(IOracle.Request memory _request) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_request)); + } + + /** + * @notice Computes the ID of a given response as it's done in the Oracle + * + * @param _response The response to compute the ID for + * @return _id The ID of the response + */ + function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_response)); + } + + /** + * @notice Computes the ID of a given dispute as it's done in the Oracle + * + * @param _dispute The dispute to compute the ID for + * @return _id The ID of the dispute + */ + function _getId(IOracle.Dispute memory _dispute) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_dispute)); + } +} diff --git a/yarn.lock b/yarn.lock index c61b7709..18287d95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -192,10 +192,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@defi-wonderland/prophet-core-contracts@0.0.0-d05a00d0": - version "0.0.0-d05a00d0" - resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-d05a00d0.tgz#1357d917fe46a5a12faa67f557e990255dda14fd" - integrity sha512-F/y0r/qDLFACzsN7Y2VRAPIS9Yhx2btU/m7cQT7T84TbIxAmBGVw6/7nb+HeIbXh+QDO90RP6vHAdQOow/q1Xw== +"@defi-wonderland/prophet-core-contracts@0.0.0-a1d2cc55": + version "0.0.0-a1d2cc55" + resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-a1d2cc55.tgz#e0bba63cdb143ffba6721049d2b0577eb39329fb" + integrity sha512-gl+8QvkzPd144yESzXhl2ceJ5blZczKh7HLioSfJ1uZnrJAR90z/MJD1fCb/1Q3oDyaq6WMWnJSK9VvMkadTtQ== dependencies: "@defi-wonderland/solidity-utils" "0.0.0-3e9c8e8b" "@openzeppelin/contracts" "^4.9.3" @@ -324,9 +324,9 @@ ts-essentials "^7.0.1" "@types/minimist@^1.2.0": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.4.tgz#81f886786411c45bba3f33e781ab48bd56bfca2e" - integrity sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@20.5.1": version "20.5.1" @@ -334,9 +334,9 @@ integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== "@types/normalize-package-data@^2.4.0": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz#291c243e4b94dbfbc0c0ee26b7666f1d5c030e2c" - integrity sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/prettier@^2.1.1": version "2.7.3" @@ -357,9 +357,9 @@ acorn-jsx@^5.0.0: integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== acorn@^6.0.7: version "6.4.2" @@ -367,9 +367,9 @@ acorn@^6.0.7: integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^8.4.1: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== add-stream@^1.0.0: version "1.0.0" @@ -1457,9 +1457,9 @@ fast-diff@^1.1.2, fast-diff@^1.2.0: integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== fast-glob@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -1576,7 +1576,7 @@ for-each@^0.3.3: "forge-std@https://github.com/foundry-rs/forge-std": version "1.7.1" - resolved "https://github.com/foundry-rs/forge-std#267acd30a625086b3f16e1a28cfe0c5097fa46b8" + resolved "https://github.com/foundry-rs/forge-std#37a37ab73364d6644bfe11edf88a07880f99bd56" "forge-std@https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421": version "1.7.1" @@ -2858,9 +2858,9 @@ progress@^2.0.0: integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== q@^1.5.1: version "1.5.1" @@ -3787,9 +3787,9 @@ universalify@^0.1.0: integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== uri-js@^4.2.2: version "4.4.1" @@ -3916,9 +3916,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^2.2.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" - integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" From c6d4e8d6e9b97ecccc0e478e65f3d6e904f8b510 Mon Sep 17 00:00:00 2001 From: Gas <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:47:27 +0400 Subject: [PATCH 32/53] perf: optimize `CallbackModule` (#5) --- .../modules/finality/callback_module.md | 5 +- .../modules/finality/CallbackModule.sol | 64 ++--- .../modules/finality/ICallbackModule.sol | 128 +++++---- .../modules/finality/CallbackModule.t.sol | 260 ++++++++---------- solidity/test/utils/Helpers.sol | 1 - yarn.lock | 6 +- 6 files changed, 209 insertions(+), 255 deletions(-) diff --git a/docs/src/content/modules/finality/callback_module.md b/docs/src/content/modules/finality/callback_module.md index e2c1ce9c..78409c9a 100644 --- a/docs/src/content/modules/finality/callback_module.md +++ b/docs/src/content/modules/finality/callback_module.md @@ -10,8 +10,8 @@ The Callback Module is a finality module that allows users to call a function on ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: Returns the decoded data for a request. -- `finalizeRequest(bytes32 _requestId, address)`: Executing the callback call on the target. +- `decodeRequestData`: Returns the decoded data for a request. +- `finalizeRequest`: Executing the callback call on the target. ### Request Parameters @@ -25,4 +25,3 @@ As any finality module, the `CallbackModule` implements the `finalizeRequest` fu ## 4. Gotchas - The success of the callback call in `finalizeRequest` is purposely not checked, specifying a function or parameters that lead to a revert will not stop the request from being finalized. -- The target must be a contract. diff --git a/solidity/contracts/modules/finality/CallbackModule.sol b/solidity/contracts/modules/finality/CallbackModule.sol index 3f3faf9d..f3eea3d9 100644 --- a/solidity/contracts/modules/finality/CallbackModule.sol +++ b/solidity/contracts/modules/finality/CallbackModule.sol @@ -1,42 +1,34 @@ -// // 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 {ICallbackModule} from '../../../interfaces/modules/finality/ICallbackModule.sol'; +import {ICallbackModule} from '../../../interfaces/modules/finality/ICallbackModule.sol'; -// contract CallbackModule is Module, ICallbackModule { -// constructor(IOracle _oracle) Module(_oracle) {} +contract CallbackModule is Module, ICallbackModule { + constructor(IOracle _oracle) Module(_oracle) {} -// /// @inheritdoc IModule -// function moduleName() public pure returns (string memory _moduleName) { -// _moduleName = 'CallbackModule'; -// } + /// @inheritdoc IModule + function moduleName() public pure returns (string memory _moduleName) { + _moduleName = 'CallbackModule'; + } -// /// @inheritdoc ICallbackModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } + /// @inheritdoc ICallbackModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } -// /** -// * @notice Checks if the target address has code (i.e. is a contract) -// * @param _data The encoded data for the request -// */ -// function _afterSetupRequest(bytes32, bytes calldata _data) internal view override { -// RequestParameters memory _params = abi.decode(_data, (RequestParameters)); -// if (_params.target.code.length == 0) revert CallbackModule_TargetHasNoCode(); -// } - -// /// @inheritdoc ICallbackModule -// function finalizeRequest( -// bytes32 _requestId, -// address _finalizer -// ) external override(Module, ICallbackModule) onlyOracle { -// RequestParameters memory _params = decodeRequestData(_requestId); -// _params.target.call(_params.data); -// emit Callback(_requestId, _params.target, _params.data); -// emit RequestFinalized(_requestId, _finalizer); -// } -// } + /// @inheritdoc ICallbackModule + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external override(Module, ICallbackModule) onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.finalityModuleData); + _params.target.call(_params.data); + emit Callback(_response.requestId, _params.target, _params.data); + emit RequestFinalized(_response.requestId, _response, _finalizer); + } +} diff --git a/solidity/interfaces/modules/finality/ICallbackModule.sol b/solidity/interfaces/modules/finality/ICallbackModule.sol index 99b298ac..d5cb064f 100644 --- a/solidity/interfaces/modules/finality/ICallbackModule.sol +++ b/solidity/interfaces/modules/finality/ICallbackModule.sol @@ -1,65 +1,63 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IFinalityModule} from -// '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/finality/IFinalityModule.sol'; - -// /** -// * @title CallbackModule -// * @notice Module allowing users to call a function on a contract -// * as a result of a request being finalized. -// */ -// interface ICallbackModule is IFinalityModule { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice A callback has been executed -// * @param _requestId The id of the request being finalized -// * @param _target The target address for the callback -// * @param _data The calldata forwarded to the _target -// */ -// event Callback(bytes32 indexed _requestId, address indexed _target, bytes _data); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Thrown when the target address has no code (i.e. is not a contract) -// */ -// error CallbackModule_TargetHasNoCode(); - -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Parameters of the request as stored in the module -// * @param target The target address for the callback -// * @param data The calldata forwarded to the _target -// */ -// struct RequestParameters { -// address target; -// bytes data; -// } - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @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 Finalizes the request by executing the callback call on the target -// * @dev The success of the callback call is purposely not checked -// * @param _requestId The id of the request -// */ -// function finalizeRequest(bytes32 _requestId, address) external; -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IFinalityModule} from + '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/finality/IFinalityModule.sol'; + +/** + * @title CallbackModule + * @notice Module allowing users to call a function on a contract + * as a result of a request being finalized. + */ +interface ICallbackModule is IFinalityModule { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A callback has been executed + * @param _requestId The id of the request being finalized + * @param _target The target address for the callback + * @param _data The calldata forwarded to the _target + */ + event Callback(bytes32 indexed _requestId, address indexed _target, bytes _data); + + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Parameters of the request as stored in the module + * @param target The target address for the callback + * @param data The calldata forwarded to the _target + */ + struct RequestParameters { + address target; + bytes data; + } + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @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 the request by executing the callback call on the target + * @dev The success of the callback call is purposely not checked + * @param _request The request being finalized + * @param _response The final response + * @param _finalizer The address that initiated the finalization + */ + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external; +} diff --git a/solidity/test/unit/modules/finality/CallbackModule.t.sol b/solidity/test/unit/modules/finality/CallbackModule.t.sol index 77bf4c8a..6cbf2076 100644 --- a/solidity/test/unit/modules/finality/CallbackModule.t.sol +++ b/solidity/test/unit/modules/finality/CallbackModule.t.sol @@ -1,147 +1,113 @@ -// // SPDX-License-Identifier: AGPL-3.0-only -// pragma solidity ^0.8.19; - -// import 'forge-std/Test.sol'; - -// import {Helpers} from '../../../utils/Helpers.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 {CallbackModule, ICallbackModule} from '../../../../contracts/modules/finality/CallbackModule.sol'; - -// /** -// * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks -// */ -// contract ForTest_CallbackModule is CallbackModule { -// constructor(IOracle _oracle) CallbackModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } -// } - -// /** -// * @title Callback Module Unit tests -// */ -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_CallbackModule public callbackModule; -// // A mock oracle -// IOracle public oracle; - -// event Callback(bytes32 indexed _request, address indexed _target, bytes _data); -// 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'); - -// callbackModule = new ForTest_CallbackModule(oracle); -// } -// } - -// contract CallbackModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(callbackModule.moduleName(), 'CallbackModule'); -// } - -// /** -// * @notice Test that the decodeRequestData function returns the correct values -// */ -// function test_decodeRequestData(bytes32 _requestId, address _target, bytes memory _data) public { -// // Create and set some mock request data -// bytes memory _requestData = abi.encode(ICallbackModule.RequestParameters({target: _target, data: _data})); -// callbackModule.forTest_setRequestData(_requestId, _requestData); - -// // Decode the given request data -// ICallbackModule.RequestParameters memory _params = callbackModule.decodeRequestData(_requestId); - -// // Check: decoded values match original values? -// assertEq(_params.target, _target); -// assertEq(_params.data, _data); -// } -// } - -// contract CallbackModule_Unit_FinalizeRequest is BaseTest { -// /** -// * @notice Test that finalizeRequest calls the _target.callback with the correct data -// */ -// function test_triggersCallback( -// bytes32 _requestId, -// address _target, -// bytes calldata _data -// ) public assumeFuzzable(_target) { -// vm.assume(_target != address(vm)); - -// // Create and set some mock request data -// bytes memory _requestData = abi.encode(ICallbackModule.RequestParameters({target: _target, data: _data})); -// callbackModule.forTest_setRequestData(_requestId, _requestData); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(callbackModule)); -// emit Callback(_requestId, _target, _data); - -// vm.prank(address(oracle)); -// callbackModule.finalizeRequest(_requestId, address(oracle)); -// } - -// function test_emitsEvent(bytes32 _requestId, address _target, bytes calldata _data) public assumeFuzzable(_target) { -// vm.assume(_target != address(vm)); - -// // Create and set some mock request data -// bytes memory _requestData = abi.encode(ICallbackModule.RequestParameters({target: _target, data: _data})); -// callbackModule.forTest_setRequestData(_requestId, _requestData); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(callbackModule)); -// emit RequestFinalized(_requestId, address(oracle)); - -// vm.prank(address(oracle)); -// callbackModule.finalizeRequest(_requestId, address(oracle)); -// } - -// /** -// * @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); -// callbackModule.finalizeRequest(_requestId, address(_caller)); -// } -// } - -// contract CallbackModule_Unit_Setup is BaseTest { -// /** -// * @notice Test that _afterSetupRequest checks if the target address is a contract. -// */ -// function test_revertIfTargetNotContract( -// bytes32 _requestId, -// address _target, -// bool _hasCode, -// bytes calldata _data -// ) public assumeFuzzable(_target) { -// vm.assume(_target.code.length == 0); -// bytes memory _requestData = abi.encode(ICallbackModule.RequestParameters({target: _target, data: _data})); - -// if (_hasCode) { -// vm.etch(_target, hex'069420'); -// } else { -// // Check: does it revert if the target has no code? -// vm.expectRevert(ICallbackModule.CallbackModule_TargetHasNoCode.selector); -// } - -// vm.prank(address(oracle)); -// callbackModule.setupRequest(_requestId, _requestData); -// } -// } +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; + +import {Helpers} from '../../../utils/Helpers.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 {CallbackModule, ICallbackModule} from '../../../../contracts/modules/finality/CallbackModule.sol'; + +/** + * @title Callback Module Unit tests + */ +contract BaseTest is Test, Helpers { + // The target contract + CallbackModule public callbackModule; + // A mock oracle + IOracle public oracle; + + // Events + event Callback(bytes32 indexed _request, address indexed _target, bytes _data); + + /** + * @notice Deploy the target and mock oracle+accounting extension + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + callbackModule = new CallbackModule(oracle); + } +} + +contract CallbackModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(callbackModule.moduleName(), 'CallbackModule'); + } + + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData(address _target, bytes memory _data) public { + // Create and set some mock request data + bytes memory _requestData = abi.encode(ICallbackModule.RequestParameters({target: _target, data: _data})); + + // Decode the given request data + ICallbackModule.RequestParameters memory _params = callbackModule.decodeRequestData(_requestData); + + // Check: decoded values match original values? + assertEq(_params.target, _target); + assertEq(_params.data, _data); + } +} + +contract CallbackModule_Unit_FinalizeRequest is BaseTest { + /** + * @notice Test that finalizeRequest emits events + */ + function test_emitsEvents(address _proposer, address _target, bytes calldata _data) public assumeFuzzable(_target) { + mockRequest.finalityModuleData = abi.encode(ICallbackModule.RequestParameters({target: _target, data: _data})); + mockResponse.requestId = _getId(mockRequest); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(callbackModule)); + emit Callback(mockResponse.requestId, _target, _data); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(callbackModule)); + emit RequestFinalized(mockResponse.requestId, mockResponse, _proposer); + + vm.prank(address(oracle)); + callbackModule.finalizeRequest(mockRequest, mockResponse, _proposer); + } + + /** + * @notice Test that finalizeRequest triggers the callback + */ + function test_triggersCallback( + address _proposer, + address _target, + bytes calldata _data + ) public assumeFuzzable(_target) { + mockRequest.finalityModuleData = abi.encode(ICallbackModule.RequestParameters({target: _target, data: _data})); + mockResponse.requestId = _getId(mockRequest); + + // Mock and expect the callback + _mockAndExpect(_target, _data, abi.encode('')); + + vm.prank(address(oracle)); + callbackModule.finalizeRequest(mockRequest, mockResponse, _proposer); + } + + /** + * @notice Test that the finalizeRequest reverts if caller is not the oracle + */ + function test_revertsIfWrongCaller(ICallbackModule.RequestParameters calldata _data, address _caller) public { + vm.assume(_caller != address(oracle)); + + mockRequest.finalityModuleData = abi.encode(_data); + mockResponse.requestId = _getId(mockRequest); + + // Check: does it revert if not called by the Oracle? + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + + vm.prank(_caller); + callbackModule.finalizeRequest(mockRequest, mockResponse, _caller); + } +} diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 213a0064..4f22491b 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -53,7 +53,6 @@ contract Helpers is DSTestPlus, TestConstants { vm.expectCall(_receiver, _calldata); } - // TODO: What does _balanceIncrease do? function _forBondDepositERC20( IAccountingExtension _accountingExtension, address _depositor, diff --git a/yarn.lock b/yarn.lock index 18287d95..4e936a9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -274,9 +274,9 @@ antlr4ts "^0.5.0-alpha.4" "@solidity-parser/parser@^0.16.0": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.1.tgz#f7c8a686974e1536da0105466c4db6727311253c" - integrity sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw== + version "0.16.2" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.2.tgz#42cb1e3d88b3e8029b0c9befff00b634cd92d2fa" + integrity sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg== dependencies: antlr4ts "^0.5.0-alpha.4" From 6eb1aff7ecd11820eb379000911772b751da0921 Mon Sep 17 00:00:00 2001 From: Gas <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:50:13 +0400 Subject: [PATCH 33/53] perf: optimize `CircuitResolverModule` (#6) --- .../dispute/circuit_resolver_module.md | 7 +- .../modules/dispute/CircuitResolverModule.sol | 184 +++-- .../dispute/ICircuitResolverModule.sol | 144 ++-- solidity/test/mocks/MockVerifier.sol | 15 + .../dispute/CircuitResolverModule.t.sol | 775 +++++++----------- 5 files changed, 494 insertions(+), 631 deletions(-) create mode 100644 solidity/test/mocks/MockVerifier.sol 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); + } +} From c96f6cdc6d397f1a5823ebdbf2e92f655d300abf Mon Sep 17 00:00:00 2001 From: Gas <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:57:12 +0400 Subject: [PATCH 34/53] perf: optimize `RootVerificationModule` (#7) --- .../dispute/root_verification_module.md | 7 +- .../dispute/RootVerificationModule.sol | 189 ++--- .../dispute/IRootVerificationModule.sol | 151 ++-- .../dispute/RootVerificationModule.t.sol | 660 ++++++++---------- yarn.lock | 22 +- 5 files changed, 485 insertions(+), 544 deletions(-) diff --git a/docs/src/content/modules/dispute/root_verification_module.md b/docs/src/content/modules/dispute/root_verification_module.md index 6bb6e1f0..b2ad1935 100644 --- a/docs/src/content/modules/dispute/root_verification_module.md +++ b/docs/src/content/modules/dispute/root_verification_module.md @@ -10,10 +10,9 @@ The Root Verification Module is a pre-dispute module that allows disputers to ca ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: Returns the decoded data for a request. -- `disputeResponse(bytes32 _requestId, bytes32 _responseId, address _disputer, address _proposer)`: Calculates the correct root 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 root 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`: Calculates the correct root 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 root as a response and finalizing the request. ### Request Parameters diff --git a/solidity/contracts/modules/dispute/RootVerificationModule.sol b/solidity/contracts/modules/dispute/RootVerificationModule.sol index e19f1cf0..e954fc1a 100644 --- a/solidity/contracts/modules/dispute/RootVerificationModule.sol +++ b/solidity/contracts/modules/dispute/RootVerificationModule.sol @@ -1,94 +1,95 @@ -// // 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 {IRootVerificationModule} from '../../../interfaces/modules/dispute/IRootVerificationModule.sol'; -// import {MerkleLib} from '../../libraries/MerkleLib.sol'; - -// contract RootVerificationModule is Module, IRootVerificationModule { -// using MerkleLib for MerkleLib.Tree; - -// /** -// * @notice The calculated correct root for a given request -// */ -// mapping(bytes32 _requestId => bytes32 _correctRoot) internal _correctRoots; - -// constructor(IOracle _oracle) Module(_oracle) {} - -// /// @inheritdoc IModule -// function moduleName() external pure returns (string memory _moduleName) { -// return 'RootVerificationModule'; -// } - -// /// @inheritdoc IRootVerificationModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } - -// /// @inheritdoc IRootVerificationModule -// function disputeEscalated(bytes32 _disputeId) external onlyOracle {} - -// /// @inheritdoc IRootVerificationModule -// function onDisputeStatusChange(bytes32, IOracle.Dispute memory _dispute) external onlyOracle { -// RequestParameters memory _params = decodeRequestData(_dispute.requestId); - -// IOracle.Response memory _response = ORACLE.getResponse(_dispute.responseId); - -// bool _won = abi.decode(_response.response, (bytes32)) != _correctRoots[_dispute.requestId]; - -// 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(_correctRoots[_dispute.requestId])); -// ORACLE.finalize(_dispute.requestId, _correctResponseId); -// } else { -// ORACLE.finalize(_dispute.requestId, _dispute.responseId); -// } - -// delete _correctRoots[_dispute.requestId]; - -// emit DisputeStatusChanged({ -// _requestId: _dispute.requestId, -// _responseId: _dispute.responseId, -// _disputer: _dispute.disputer, -// _proposer: _dispute.proposer, -// _status: _dispute.status -// }); -// } - -// /// @inheritdoc IRootVerificationModule -// 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); - -// bytes32 _correctRoot = _params.treeVerifier.calculateRoot(_params.treeData, _params.leavesToInsert); -// _correctRoots[_requestId] = _correctRoot; - -// bool _won = abi.decode(_response.response, (bytes32)) != _correctRoot; - -// _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 {IRootVerificationModule} from '../../../interfaces/modules/dispute/IRootVerificationModule.sol'; +import {MerkleLib} from '../../libraries/MerkleLib.sol'; + +contract RootVerificationModule is Module, IRootVerificationModule { + using MerkleLib for MerkleLib.Tree; + + /** + * @notice The calculated correct root for a given request + */ + mapping(bytes32 _requestId => bytes32 _correctRoot) internal _correctRoots; + + constructor(IOracle _oracle) Module(_oracle) {} + + /// @inheritdoc IModule + function moduleName() external pure returns (string memory _moduleName) { + return 'RootVerificationModule'; + } + + /// @inheritdoc IRootVerificationModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } + + /// @inheritdoc IRootVerificationModule + 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 + RequestParameters memory _params = decodeRequestData(_request.disputeModuleData); + + bytes32 _correctRoot = _correctRoots[_dispute.requestId]; + bool _won = abi.decode(_response.response, (bytes32)) != _correctRoot; + + 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: abi.encode(_correctRoot) + }); + + 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 _correctRoots[_dispute.requestId]; + } + + /// @inheritdoc IRootVerificationModule + function disputeResponse( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.disputeModuleData); + + bytes32 _correctRoot = _params.treeVerifier.calculateRoot(_params.treeData, _params.leavesToInsert); + _correctRoots[_response.requestId] = _correctRoot; + + IOracle.DisputeStatus _status = + abi.decode(_response.response, (bytes32)) != _correctRoot ? 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/IRootVerificationModule.sol b/solidity/interfaces/modules/dispute/IRootVerificationModule.sol index 682468eb..f29c92f1 100644 --- a/solidity/interfaces/modules/dispute/IRootVerificationModule.sol +++ b/solidity/interfaces/modules/dispute/IRootVerificationModule.sol @@ -1,84 +1,81 @@ -// // 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 {ITreeVerifier} from '../../ITreeVerifier.sol'; -// import {IAccountingExtension} from '../../extensions/IAccountingExtension.sol'; +import {ITreeVerifier} from '../../ITreeVerifier.sol'; +import {IAccountingExtension} from '../../extensions/IAccountingExtension.sol'; -// /* -// * @title RootVerificationModule -// * @notice Dispute module allowing disputers to calculate the correct root -// * for a given request and propose it as a response. If the disputer wins the -// * dispute, he is rewarded with the bond of the proposer. -// * @dev This module is a pre-dispute module. It allows disputing -// * and resolving a response in a single call. -// */ -// interface IRootVerificationModule is IDisputeModule { -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ +/* + * @title RootVerificationModule + * @notice Dispute module allowing disputers to calculate the correct root for a given request and propose it as a response. + * If the disputer wins the dispute, he is rewarded with the bond of the proposer. + * + * @dev This module is a pre-dispute module. It allows disputing and resolving a response in a single call. + */ +interface IRootVerificationModule is IDisputeModule { + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Parameters of the request as stored in the module -// * @param treeData The data of the tree -// * @param leavesToInsert The leaves to insert in the tree -// * @param treeVerifier The tree verifier to use to calculate the correct root -// * @param accountingExtension The accounting extension to use for bonds and payments -// * @param bondToken The token to use for bonds and payments -// * @param bondSize The size of the bond to participate in the request -// */ -// struct RequestParameters { -// bytes treeData; -// bytes32[] leavesToInsert; -// ITreeVerifier treeVerifier; -// IAccountingExtension accountingExtension; -// IERC20 bondToken; -// uint256 bondSize; -// } -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ + /** + * @notice Parameters of the request as stored in the module + * @param treeData The data of the tree + * @param leavesToInsert The leaves to insert in the tree + * @param treeVerifier The tree verifier to use to calculate the correct root + * @param accountingExtension The accounting extension to use for bonds and payments + * @param bondToken The token to use for bonds and payments + * @param bondSize The size of the bond to participate in the request + */ + struct RequestParameters { + bytes treeData; + bytes32[] leavesToInsert; + ITreeVerifier treeVerifier; + IAccountingExtension accountingExtension; + IERC20 bondToken; + uint256 bondSize; + } + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ -// /** -// * @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 decoded parameters of the request + */ + function decodeRequestData(bytes calldata _data) external view returns (RequestParameters memory _params); -// /** -// * @notice Calculates the correct root and compares it to the proposed one. -// * @dev Since this is a pre-dispute module, the dispute status is updated after checking -// * if the disputed response is indeed wrong, since it is calculated on dispute. -// * @param _requestId The id of the request from which the response is being disputed -// * @param _responseId The id of the response being disputed -// * @param _disputer The user who is disputing the response -// * @param _proposer The proposer of the response being disputed -// * @return _dispute The dispute of the current response with the updated status -// */ -// function disputeResponse( -// bytes32 _requestId, -// bytes32 _responseId, -// address _disputer, -// address _proposer -// ) external returns (IOracle.Dispute memory _dispute); + /** + * @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 Updates the status of the dispute and resolves it by proposing the correct root -// * as a response and finalizing the request. -// * @dev The correct root is retrieved from storage and compared to the proposed root. -// * If the dispute is won, the disputer is paid. In both cases, the request is finalized. -// * @param _dispute The dispute of the current response -// */ -// function onDisputeStatusChange(bytes32, IOracle.Dispute memory _dispute) external; - -// /** -// * @dev This function is present to comply with the module interface but it -// * is not implemented since this is a pre-dispute module. -// */ -// function disputeEscalated(bytes32 _disputeId) 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 that the response was proposed to + * @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/unit/modules/dispute/RootVerificationModule.t.sol b/solidity/test/unit/modules/dispute/RootVerificationModule.t.sol index 21ec848d..7959bd10 100644 --- a/solidity/test/unit/modules/dispute/RootVerificationModule.t.sol +++ b/solidity/test/unit/modules/dispute/RootVerificationModule.t.sol @@ -1,356 +1,304 @@ -// // 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 { -// RootVerificationModule, -// IRootVerificationModule -// } from '../../../../contracts/modules/dispute/RootVerificationModule.sol'; - -// import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; -// import {ITreeVerifier} from '../../../../interfaces/ITreeVerifier.sol'; - -// /** -// * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks -// */ -// contract ForTest_RootVerificationModule is RootVerificationModule { -// constructor(IOracle _oracle) RootVerificationModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } -// } - -// /** -// * @title Root Verification Module Unit tests -// */ -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_RootVerificationModule public rootVerificationModule; -// // 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 tree verifier -// ITreeVerifier public treeVerifier; -// // Mock addresses -// IERC20 public _token = IERC20(makeAddr('token')); -// address public _disputer = makeAddr('disputer'); -// address public _proposer = makeAddr('proposer'); - -// // Mock request data -// bytes32[32] internal _treeBranches = [ -// bytes32('branch1'), -// bytes32('branch2'), -// bytes32('branch3'), -// bytes32('branch4'), -// bytes32('branch5'), -// bytes32('branch6'), -// bytes32('branch7'), -// bytes32('branch8'), -// bytes32('branch9'), -// bytes32('branch10'), -// bytes32('branch11'), -// bytes32('branch12'), -// bytes32('branch13'), -// bytes32('branch14'), -// bytes32('branch15'), -// bytes32('branch16'), -// bytes32('branch17'), -// bytes32('branch18'), -// bytes32('branch19'), -// bytes32('branch20'), -// bytes32('branch21'), -// bytes32('branch22'), -// bytes32('branch23'), -// bytes32('branch24'), -// bytes32('branch25'), -// bytes32('branch26'), -// bytes32('branch27'), -// bytes32('branch28'), -// bytes32('branch29'), -// bytes32('branch30'), -// bytes32('branch31'), -// bytes32('branch32') -// ]; -// uint256 internal _treeCount = 1; -// bytes internal _treeData = abi.encode(_treeBranches, _treeCount); -// bytes32[] internal _leavesToInsert = [bytes32('leave1'), bytes32('leave2')]; - -// event ResponseDisputed(bytes32 indexed _requestId, bytes32 _responseId, address _disputer, address _proposer); - -// /** -// * @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'); -// treeVerifier = ITreeVerifier(makeAddr('TreeVerifier')); -// vm.etch(address(treeVerifier), hex'069420'); - -// rootVerificationModule = new ForTest_RootVerificationModule(oracle); - -// mockDispute = IOracle.Dispute({ -// createdAt: block.timestamp, -// disputer: dude, -// responseId: mockId, -// proposer: dude, -// requestId: mockId, -// status: IOracle.DisputeStatus.Active -// }); -// } -// } - -// contract RootVerificationModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(rootVerificationModule.moduleName(), 'RootVerificationModule'); -// } - -// /** -// * @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( -// IRootVerificationModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: treeVerifier, -// accountingExtension: IAccountingExtension(_accountingExtension), -// bondToken: IERC20(_randomToken), -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// rootVerificationModule.forTest_setRequestData(_requestId, _requestData); - -// IRootVerificationModule.RequestParameters memory _params = rootVerificationModule.decodeRequestData(_requestId); - -// bytes32[32] memory _treeBranchesStored; -// uint256 _treeCountStored; -// (_treeBranchesStored, _treeCountStored) = abi.decode(_params.treeData, (bytes32[32], uint256)); - -// // Check: is the request data properly stored? -// for (uint256 _i = 0; _i < _treeBranches.length; _i++) { -// assertEq(_treeBranchesStored[_i], _treeBranches[_i], 'Mismatch: decoded tree branch'); -// } -// for (uint256 _i = 0; _i < _leavesToInsert.length; _i++) { -// assertEq(_params.leavesToInsert[_i], _leavesToInsert[_i], 'Mismatch: decoded leave to insert'); -// } -// assertEq(_treeCountStored, _treeCount, 'Mismatch: decoded tree count'); -// assertEq(address(_params.treeVerifier), address(treeVerifier), 'Mismatch: decoded tree 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'); -// } -// } - -// contract RootVerificationModule_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); - -// // Mock request data -// bytes memory _requestData = abi.encode( -// IRootVerificationModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: treeVerifier, -// accountingExtension: accountingExtension, -// bondToken: _token, -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// rootVerificationModule.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(bytes32('randomRoot')) -// }); - -// // 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 tree verifier, calculating the root -// _mockAndExpect( -// address(treeVerifier), -// abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), -// abi.encode(bytes32('randomRoot2')) -// ); - -// vm.prank(address(oracle)); -// IOracle.Dispute memory _dispute = -// rootVerificationModule.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); - -// // Mock request data -// bytes memory _requestData = abi.encode( -// IRootVerificationModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: treeVerifier, -// accountingExtension: accountingExtension, -// bondToken: _token, -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// rootVerificationModule.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(bytes32('randomRoot')) -// }); - -// // 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 tree verifier, calculating the root -// _mockAndExpect( -// address(treeVerifier), -// abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), -// abi.encode(bytes32('randomRoot2')) -// ); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(rootVerificationModule)); -// emit ResponseDisputed(_requestId, _responseId, _disputer, _proposer); - -// vm.prank(address(oracle)); -// rootVerificationModule.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 _encodedCorrectRoot = abi.encode(bytes32('randomRoot')); - -// // Mock request data -// bytes memory _requestData = abi.encode( -// IRootVerificationModule.RequestParameters({ -// treeData: _treeData, -// leavesToInsert: _leavesToInsert, -// treeVerifier: treeVerifier, -// accountingExtension: accountingExtension, -// bondToken: _token, -// bondSize: _bondSize -// }) -// ); - -// // Store the mock request -// rootVerificationModule.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: _encodedCorrectRoot -// }); - -// // 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 tree verifier, calculating the root -// _mockAndExpect( -// address(treeVerifier), -// abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), -// _encodedCorrectRoot -// ); - -// vm.prank(address(oracle)); -// IOracle.Dispute memory _dispute = -// rootVerificationModule.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: revert if not called by the Oracle? -// vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); - -// vm.prank(_randomCaller); -// rootVerificationModule.disputeResponse(mockId, mockId, dude, dude); -// } -// } - -// contract RootVerificationModule_Unit_DisputeEscalated is BaseTest { -// /** -// * @notice Test if dispute escalated do nothing -// */ -// function test_returnCorrectStatus() public { -// // Record sstore and sload -// vm.prank(address(oracle)); -// vm.record(); -// rootVerificationModule.disputeEscalated(mockId); -// (bytes32[] memory _reads, bytes32[] memory _writes) = vm.accesses(address(rootVerificationModule)); - -// // Check: no storage access? -// assertEq(_reads.length, 0); -// assertEq(_writes.length, 0); -// } -// } +// 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 { + RootVerificationModule, + IRootVerificationModule +} from '../../../../contracts/modules/dispute/RootVerificationModule.sol'; + +import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; +import {ITreeVerifier} from '../../../../interfaces/ITreeVerifier.sol'; + +/** + * @title Root Verification Module Unit tests + */ +contract BaseTest is Test, Helpers { + // The target contract + RootVerificationModule public rootVerificationModule; + // A mock oracle + IOracle public oracle; + // A mock accounting extension + IAccountingExtension public accountingExtension; + // A mock tree verifier + ITreeVerifier public treeVerifier; + // Mock addresses + IERC20 public _token = IERC20(makeAddr('token')); + + // Mock request data + bytes32[32] internal _treeBranches = [ + bytes32('branch1'), + bytes32('branch2'), + bytes32('branch3'), + bytes32('branch4'), + bytes32('branch5'), + bytes32('branch6'), + bytes32('branch7'), + bytes32('branch8'), + bytes32('branch9'), + bytes32('branch10'), + bytes32('branch11'), + bytes32('branch12'), + bytes32('branch13'), + bytes32('branch14'), + bytes32('branch15'), + bytes32('branch16'), + bytes32('branch17'), + bytes32('branch18'), + bytes32('branch19'), + bytes32('branch20'), + bytes32('branch21'), + bytes32('branch22'), + bytes32('branch23'), + bytes32('branch24'), + bytes32('branch25'), + bytes32('branch26'), + bytes32('branch27'), + bytes32('branch28'), + bytes32('branch29'), + bytes32('branch30'), + bytes32('branch31'), + bytes32('branch32') + ]; + uint256 internal _treeCount = 1; + bytes internal _treeData = abi.encode(_treeBranches, _treeCount); + bytes32[] internal _leavesToInsert = [bytes32('leave1'), bytes32('leave2')]; + + // Events + 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'); + + accountingExtension = IAccountingExtension(makeAddr('AccountingExtension')); + vm.etch(address(accountingExtension), hex'069420'); + + treeVerifier = ITreeVerifier(makeAddr('TreeVerifier')); + vm.etch(address(treeVerifier), hex'069420'); + + rootVerificationModule = new RootVerificationModule(oracle); + } +} + +contract RootVerificationModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(rootVerificationModule.moduleName(), 'RootVerificationModule'); + } + + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData_returnsCorrectData( + address _accountingExtension, + address _randomToken, + uint256 _bondSize + ) public { + // Mock data + bytes memory _requestData = abi.encode( + IRootVerificationModule.RequestParameters({ + treeData: _treeData, + leavesToInsert: _leavesToInsert, + treeVerifier: treeVerifier, + accountingExtension: IAccountingExtension(_accountingExtension), + bondToken: IERC20(_randomToken), + bondSize: _bondSize + }) + ); + + IRootVerificationModule.RequestParameters memory _params = rootVerificationModule.decodeRequestData(_requestData); + + bytes32[32] memory _treeBranchesStored; + uint256 _treeCountStored; + (_treeBranchesStored, _treeCountStored) = abi.decode(_params.treeData, (bytes32[32], uint256)); + + // Check: is the request data properly stored? + for (uint256 _i = 0; _i < _treeBranches.length; _i++) { + assertEq(_treeBranchesStored[_i], _treeBranches[_i], 'Mismatch: decoded tree branch'); + } + for (uint256 _i = 0; _i < _leavesToInsert.length; _i++) { + assertEq(_params.leavesToInsert[_i], _leavesToInsert[_i], 'Mismatch: decoded leave to insert'); + } + assertEq(_treeCountStored, _treeCount, 'Mismatch: decoded tree count'); + assertEq(address(_params.treeVerifier), address(treeVerifier), 'Mismatch: decoded tree 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'); + } +} + +contract RootVerificationModule_Unit_DisputeResponse is BaseTest { + /** + * @notice Test if dispute incorrect response returns the correct status + */ + function test_disputeIncorrectResponse( + bytes memory _treeData, + bytes32[] calldata _leavesToInsert, + IAccountingExtension _accountingExtension, + uint256 _bondSize + ) public { + mockRequest.disputeModuleData = abi.encode( + IRootVerificationModule.RequestParameters({ + treeData: _treeData, + leavesToInsert: _leavesToInsert, + treeVerifier: treeVerifier, + accountingExtension: _accountingExtension, + bondToken: _token, + bondSize: _bondSize + }) + ); + + // Create new Response memory struct with random values + mockResponse.response = abi.encode(bytes32('randomRoot')); + + // Mock and expect the call to the tree verifier, calculating the root + _mockAndExpect( + address(treeVerifier), + abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), + abi.encode(bytes32('randomRoot2')) + ); + + // 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) + ); + + vm.prank(address(oracle)); + rootVerificationModule.disputeResponse(mockRequest, mockResponse, mockDispute); + } + + function test_emitsEvent( + bytes memory _treeData, + bytes32[] calldata _leavesToInsert, + IAccountingExtension _accountingExtension, + uint256 _bondSize + ) public { + mockRequest.disputeModuleData = abi.encode( + IRootVerificationModule.RequestParameters({ + treeData: _treeData, + leavesToInsert: _leavesToInsert, + treeVerifier: treeVerifier, + accountingExtension: _accountingExtension, + bondToken: _token, + bondSize: _bondSize + }) + ); + + // Create new Response memory struct with random values + mockResponse.response = abi.encode(bytes32('randomRoot')); + mockResponse.requestId = _getId(mockRequest); + mockDispute.requestId = _getId(mockRequest); + mockDispute.responseId = _getId(mockResponse); + + // Mock and expect the call to the tree verifier, calculating the root + _mockAndExpect( + address(treeVerifier), + abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), + abi.encode(bytes32('randomRoot2')) + ); + + // 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(rootVerificationModule)); + emit ResponseDisputed({ + _requestId: mockDispute.requestId, + _responseId: mockDispute.responseId, + _disputeId: _getId(mockDispute), + _dispute: mockDispute, + _blockNumber: block.number + }); + + vm.prank(address(oracle)); + rootVerificationModule.disputeResponse(mockRequest, mockResponse, mockDispute); + } + + /** + * @notice Test if dispute correct response returns the correct status + */ + function test_disputeCorrectResponse( + bytes memory _treeData, + bytes32[] calldata _leavesToInsert, + IAccountingExtension _accountingExtension, + uint256 _bondSize + ) public { + mockRequest.disputeModuleData = abi.encode( + IRootVerificationModule.RequestParameters({ + treeData: _treeData, + leavesToInsert: _leavesToInsert, + treeVerifier: treeVerifier, + accountingExtension: _accountingExtension, + bondToken: _token, + bondSize: _bondSize + }) + ); + + bytes memory _encodedCorrectRoot = abi.encode(bytes32('randomRoot')); + + // Create new Response memory struct with random values + mockResponse.response = _encodedCorrectRoot; + mockResponse.requestId = _getId(mockRequest); + + // Mock and expect the call to the tree verifier, calculating the root + _mockAndExpect( + address(treeVerifier), + abi.encodeCall(ITreeVerifier.calculateRoot, (_treeData, _leavesToInsert)), + _encodedCorrectRoot + ); + + // 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)); + rootVerificationModule.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: revert if not called by the Oracle? + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + + vm.prank(_randomCaller); + rootVerificationModule.disputeResponse(mockRequest, mockResponse, mockDispute); + } +} diff --git a/yarn.lock b/yarn.lock index 4e936a9c..f3d82dcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1181,14 +1181,10 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -"ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": - version "1.0.0" - resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" - -"ds-test@https://github.com/dapphub/ds-test": +"ds-test@git+https://github.com/dapphub/ds-test.git", "ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" uid e282159d5170298eb2455a6c05280ab5a73a4ef0 - resolved "https://github.com/dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0" + resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" "ds-test@https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" @@ -1570,14 +1566,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +"forge-std@git+https://github.com/foundry-rs/forge-std.git": + version "1.7.1" + resolved "git+https://github.com/foundry-rs/forge-std.git#37a37ab73364d6644bfe11edf88a07880f99bd56" + "forge-std@git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e": version "1.5.6" resolved "git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e" -"forge-std@https://github.com/foundry-rs/forge-std": - version "1.7.1" - resolved "https://github.com/foundry-rs/forge-std#37a37ab73364d6644bfe11edf88a07880f99bd56" - "forge-std@https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421": version "1.7.1" resolved "https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421" @@ -3265,9 +3261,9 @@ solhint@3.5.1: prettier "^2.8.3" solidity-ast@^0.4.38: - version "0.4.52" - resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.52.tgz#9f1a9abc7e5ba28bbf91146ecd07aec7e70f3c85" - integrity sha512-iOya9BSiB9jhM8Vf40n8lGELGzwrUc57rl5BhfNtJ5cvAaMvRcNlHeAMNvqJJyjoUnczqRbHqdivEqK89du3Cw== + version "0.4.53" + resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.53.tgz#098259472fccd234ff00f050afaf7843a7ccd635" + integrity sha512-/7xYF//mAt4iP9S21fCFSLMouYXzXJqrd84jbI1LHL1rq7XhyFLUXxVcRkl9KqEEQmI+DmDbTeS6W5cEwcJ2wQ== dependencies: array.prototype.findlast "^1.2.2" From 6ce0907148b5e1a1efe7e4334d388ff9f25ce40c Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 16:52:22 +0400 Subject: [PATCH 35/53] docs: natspec and documentation --- .../modules/resolution/arbitrator_module.md | 10 ++--- .../modules/resolution/IArbitratorModule.sol | 42 ++++++++++--------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/docs/src/content/modules/resolution/arbitrator_module.md b/docs/src/content/modules/resolution/arbitrator_module.md index dac2c04c..a46327f6 100644 --- a/docs/src/content/modules/resolution/arbitrator_module.md +++ b/docs/src/content/modules/resolution/arbitrator_module.md @@ -10,11 +10,11 @@ The Arbitrator Module is a part of the dispute resolution system. It allows an e ### Key Methods -- `getStatus(bytes32 _disputeId)`: Returns the arbitration status of a dispute. -- `isValid(bytes32 _disputeId)`: Indicates whether the dispute has been arbitrated. -- `startResolution(bytes32 _disputeId)`: Starts the arbitration process by calling `resolve` on the arbitrator and flags the dispute as `Active`. -- `resolveDispute(bytes32 _disputeId)`: Resolves the dispute by getting the answer from the arbitrator and notifying the oracle. -- `decodeRequestData(bytes calldata _data)`: Returns the decoded data for a request. +- `getStatus`: Returns the arbitration status of a dispute. +- `isValid`: Indicates whether the dispute has been arbitrated. +- `startResolution`: Starts the arbitration process by calling `resolve` on the arbitrator and flags the dispute as `Active`. +- `resolveDispute`: Resolves the dispute by getting the answer from the arbitrator and notifying the oracle. +- `decodeRequestData`: Returns the decoded data for a request. ### Request Parameters diff --git a/solidity/interfaces/modules/resolution/IArbitratorModule.sol b/solidity/interfaces/modules/resolution/IArbitratorModule.sol index 17e9747d..3582fa54 100644 --- a/solidity/interfaces/modules/resolution/IArbitratorModule.sol +++ b/solidity/interfaces/modules/resolution/IArbitratorModule.sol @@ -7,19 +7,13 @@ import {IResolutionModule} from /* * @title ArbitratorModule - * @notice Module allowing an external arbitrator contract - * to resolve a dispute. + * @notice Module allowing an external arbitrator contract to resolve a dispute. */ interface IArbitratorModule is IResolutionModule { /*/////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ - /** - * @notice Thrown when an unauthorized caller calls a function only the arbitrator can call - */ - error ArbitratorModule_OnlyArbitrator(); - /** * @notice Thrown when trying to resolve a dispute that is not escalated */ @@ -63,19 +57,31 @@ interface IArbitratorModule is IResolutionModule { LOGIC //////////////////////////////////////////////////////////////*/ + /** + * @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 Returns the current arbitration status of a dispute + * * @param _disputeId The ID of the dispute * @return _disputeStatus The `ArbitrationStatus` of the dispute */ function getStatus(bytes32 _disputeId) external view returns (ArbitrationStatus _disputeStatus); /** - * @notice Starts the arbitration process by calling `resolve` on the - * arbitrator and flags the dispute as Active + * @notice Starts the arbitration process by calling `resolve` on the arbitrator and flags the dispute as Active + * * @dev Only callable by the Oracle * @dev Will revert if the arbitrator address is the address zero - * @param _disputeId The ID of the dispute + * @param _disputeId The ID of the dispute + * @param _request The request + * @param _response The disputed response + * @param _dispute The dispute being sent to the resolution */ function startResolution( bytes32 _disputeId, @@ -85,10 +91,13 @@ interface IArbitratorModule is IResolutionModule { ) external; /** - * @notice Resolves the dispute by getting the answer from the arbitrator - * and updating the dispute status + * @notice Resolves the dispute by getting the answer from the arbitrator and updating the dispute status + * * @dev Only callable by the Oracle - * @param _disputeId The ID of the dispute + * @param _disputeId The ID of the dispute + * @param _request The request + * @param _response The disputed response + * @param _dispute The dispute that is being resolved */ function resolveDispute( bytes32 _disputeId, @@ -96,11 +105,4 @@ interface IArbitratorModule is IResolutionModule { IOracle.Response calldata _response, IOracle.Dispute calldata _dispute ) external; - - /** - * @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); } From da99cdd55d406516eadd576f131b93452f3d60a5 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 16:53:43 +0400 Subject: [PATCH 36/53] test: fix unit tests --- .../modules/resolution/ArbitratorModule.t.sol | 147 +++++++++--------- 1 file changed, 76 insertions(+), 71 deletions(-) diff --git a/solidity/test/unit/modules/resolution/ArbitratorModule.t.sol b/solidity/test/unit/modules/resolution/ArbitratorModule.t.sol index 66584655..eca711aa 100644 --- a/solidity/test/unit/modules/resolution/ArbitratorModule.t.sol +++ b/solidity/test/unit/modules/resolution/ArbitratorModule.t.sol @@ -35,13 +35,8 @@ contract BaseTest is Test, Helpers { IOracle public oracle; // A mock arbitrator IArbitrator public arbitrator; - // Create a new dummy dispute - IOracle.Dispute public mockDispute; - // Create a new dummy response - IOracle.Response public mockResponse; - address internal _proposer = makeAddr('proposer'); - bytes32 public mockId = bytes32('69'); + // Events event ResolutionStarted(bytes32 indexed _requestId, bytes32 indexed _disputeId); event DisputeResolved(bytes32 indexed _requestId, bytes32 indexed _disputeId, IOracle.DisputeStatus _status); @@ -56,15 +51,6 @@ contract BaseTest is Test, Helpers { vm.etch(address(arbitrator), hex'069420'); arbitratorModule = new ForTest_ArbitratorModule(oracle); - - mockDispute = IOracle.Dispute({ - disputer: makeAddr('disputer'), - proposer: makeAddr('proposer'), - responseId: bytes32('69'), - requestId: bytes32('69') - }); - - mockResponse = IOracle.Response({proposer: _proposer, requestId: mockId, response: bytes('')}); } } @@ -81,7 +67,7 @@ contract ArbitratorModule_Unit_ModuleData is BaseTest { function test_decodeRequestData(address _arbitrator) public { // Mock data - bytes memory _requestData = abi.encode(address(_arbitrator)); + bytes memory _requestData = abi.encode(_arbitrator); // Test: decode the given request data IArbitratorModule.RequestParameters memory _requestParameters = arbitratorModule.decodeRequestData(_requestData); @@ -109,64 +95,64 @@ contract ArbitratorModule_Unit_StartResolution is BaseTest { /** * @notice Test that the escalate function works as expected */ - function test_startResolution(bytes32 _disputeId, IOracle.Request calldata _request) public { - // Mock and expect the dummy dispute - mockDispute.requestId = _getId(_request); + function test_startResolution(address _arbitrator) public assumeFuzzable(_arbitrator) { + mockRequest.resolutionModuleData = abi.encode(_arbitrator); + bytes32 _requestId = _getId(mockRequest); - // Store the requestData - bytes memory _requestData = abi.encode(address(arbitrator)); + mockResponse.requestId = _requestId; + mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); // Mock and expect the callback to the arbitrator - _mockAndExpect(address(arbitrator), abi.encodeCall(arbitrator.resolve, (_disputeId)), abi.encode(bytes(''))); + _mockAndExpect(_arbitrator, abi.encodeCall(arbitrator.resolve, (_disputeId)), abi.encode(bytes(''))); vm.prank(address(oracle)); - arbitratorModule.startResolution(_disputeId, _request, mockResponse, mockDispute); + arbitratorModule.startResolution(_disputeId, mockRequest, mockResponse, mockDispute); // Check: is status now Escalated? assertEq(uint256(arbitratorModule.getStatus(_disputeId)), uint256(IArbitratorModule.ArbitrationStatus.Active)); } - function test_emitsEvent(bytes32 _disputeId, IOracle.Request calldata _request) public { - // Mock and expect the dummy dispute - mockDispute.requestId = _getId(_request); - - // Store the requestData - bytes memory _requestData = abi.encode(address(arbitrator)); + function test_emitsEvent(address _arbitrator) public assumeFuzzable(_arbitrator) { + mockRequest.resolutionModuleData = abi.encode(_arbitrator); + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); // Mock and expect the callback to the arbitrator - _mockAndExpect(address(arbitrator), abi.encodeCall(arbitrator.resolve, (_disputeId)), abi.encode(bytes(''))); + _mockAndExpect(_arbitrator, abi.encodeCall(arbitrator.resolve, (_disputeId)), abi.encode(bytes(''))); // Check: is the event emitted? vm.expectEmit(true, true, true, true, address(arbitratorModule)); emit ResolutionStarted(mockDispute.requestId, _disputeId); vm.prank(address(oracle)); - arbitratorModule.startResolution(_disputeId, _request, mockResponse, mockDispute); + arbitratorModule.startResolution(_disputeId, mockRequest, mockResponse, mockDispute); } - function test_revertInvalidCaller(address _caller, bytes32 _disputeId, IOracle.Request calldata _request) public { + function test_revertInvalidCaller(address _caller) public { vm.assume(_caller != address(oracle)); // Check: does it revert if the caller is not the Oracle? vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); vm.prank(_caller); - arbitratorModule.startResolution(_disputeId, _request, mockResponse, mockDispute); + arbitratorModule.startResolution(_getId(mockDispute), mockRequest, mockResponse, mockDispute); } - function test_revertIfEmptyArbitrator(bytes32 _disputeId, IOracle.Request calldata _request) public { - // Mock and expect the dummy dispute - mockDispute.requestId = _getId(_request); - - // Store the requestData - bytes memory _requestData = abi.encode(address(0)); + function test_revertIfEmptyArbitrator() public { + mockRequest.resolutionModuleData = abi.encode(address(0)); + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + mockDispute.requestId = _requestId; // Check: revert? vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidArbitrator.selector)); // Test: escalate the dispute vm.prank(address(oracle)); - arbitratorModule.startResolution(_disputeId, _request, mockResponse, mockDispute); + arbitratorModule.startResolution(_getId(mockDispute), mockRequest, mockResponse, mockDispute); } } @@ -174,91 +160,110 @@ contract ArbitratorModule_Unit_ResolveDispute is BaseTest { /** * @notice Test that the resolve function works as expected */ - function test_resolveDispute(bytes32 _disputeId, uint256 _status, IOracle.Request calldata _request) public { - vm.assume(_status <= uint256(IOracle.DisputeStatus.Lost)); - vm.assume(_status > uint256(IOracle.DisputeStatus.Escalated)); + function test_resolveDispute(uint256 _status, address _arbitrator) public assumeFuzzable(_arbitrator) { + _status = bound(_status, uint256(IOracle.DisputeStatus.Escalated) + 1, uint256(IOracle.DisputeStatus.Lost)); IOracle.DisputeStatus _arbitratorStatus = IOracle.DisputeStatus(_status); + mockRequest.resolutionModuleData = abi.encode(_arbitrator); + mockDispute.requestId = _getId(mockRequest); + bytes32 _disputeId = _getId(mockDispute); + + // Mock and expect IOracle.disputeStatus to be called + _mockAndExpect( + address(oracle), abi.encodeCall(oracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated) + ); + // Mock and expect getAnswer to be called on the arbitrator _mockAndExpect( - address(arbitrator), abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus) + address(_arbitrator), abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus) ); // Mock and expect IOracle.updateDisputeStatus to be called _mockAndExpect( address(oracle), - abi.encodeCall(oracle.updateDisputeStatus, (_request, mockResponse, mockDispute, _arbitratorStatus)), + abi.encodeCall(oracle.updateDisputeStatus, (mockRequest, mockResponse, mockDispute, _arbitratorStatus)), abi.encode() ); vm.prank(address(oracle)); - arbitratorModule.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + arbitratorModule.resolveDispute(_disputeId, mockRequest, mockResponse, mockDispute); // Check: is status now Resolved? assertEq(uint256(arbitratorModule.getStatus(_disputeId)), uint256(IArbitratorModule.ArbitrationStatus.Resolved)); } - function test_revertsIfInvalidResolveStatus( - bytes32 _disputeId, - uint256 _status, - IOracle.Request calldata _request - ) public { + function test_revertsIfInvalidResolveStatus(uint256 _status, address _arbitrator) public assumeFuzzable(_arbitrator) { vm.assume(_status <= uint256(IOracle.DisputeStatus.Escalated)); IOracle.DisputeStatus _arbitratorStatus = IOracle.DisputeStatus(_status); + mockRequest.resolutionModuleData = abi.encode(_arbitrator); + mockDispute.requestId = _getId(mockRequest); + bytes32 _disputeId = _getId(mockDispute); + + // Mock and expect IOracle.disputeStatus to be called + _mockAndExpect( + address(oracle), abi.encodeCall(oracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated) + ); + // Mock and expect getAnswer to be called on the arbitrator _mockAndExpect( - address(arbitrator), abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus) + address(_arbitrator), abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus) ); // Check: does it revert if the resolution status is invalid? vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidResolutionStatus.selector)); vm.prank(address(oracle)); - arbitratorModule.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + arbitratorModule.resolveDispute(_disputeId, mockRequest, mockResponse, mockDispute); } - function test_emitsEvent(bytes32 _disputeId, uint256 _status, IOracle.Request calldata _request) public { + function test_emitsEvent(uint256 _status, address _arbitrator) public assumeFuzzable(_arbitrator) { vm.assume(_status <= uint256(IOracle.DisputeStatus.Lost)); vm.assume(_status > uint256(IOracle.DisputeStatus.Escalated)); IOracle.DisputeStatus _arbitratorStatus = IOracle.DisputeStatus(_status); - // Mock and expect getAnswer to be called on the arbitrator + mockRequest.resolutionModuleData = abi.encode(_arbitrator); + mockDispute.requestId = _getId(mockRequest); + bytes32 _disputeId = _getId(mockDispute); + + // Mock and expect IOracle.disputeStatus to be called _mockAndExpect( - address(arbitrator), abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus) + address(oracle), abi.encodeCall(oracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated) ); + // Mock and expect getAnswer to be called on the arbitrator + _mockAndExpect(_arbitrator, abi.encodeCall(arbitrator.getAnswer, (_disputeId)), abi.encode(_arbitratorStatus)); + // Mock and expect IOracle.updateDisputeStatus to be called _mockAndExpect( address(oracle), - abi.encodeCall(oracle.updateDisputeStatus, (_request, mockResponse, mockDispute, _arbitratorStatus)), + abi.encodeCall(oracle.updateDisputeStatus, (mockRequest, mockResponse, mockDispute, _arbitratorStatus)), abi.encode() ); // Check: is the event emitted? vm.expectEmit(true, true, true, true, address(arbitratorModule)); - emit DisputeResolved(_getId(_request), _disputeId, _arbitratorStatus); + emit DisputeResolved(_getId(mockRequest), _disputeId, _arbitratorStatus); vm.prank(address(oracle)); - arbitratorModule.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + arbitratorModule.resolveDispute(_disputeId, mockRequest, mockResponse, mockDispute); } /** * @notice resolve dispute reverts if the dispute status isn't Active */ - function test_revertIfInvalidDispute(IOracle.Request calldata _request, bytes32 _disputeId) public { + function test_revertIfInvalidDispute(IOracle.Request calldata _request) public { // Test the 3 different invalid status (None, Won, Lost) for (uint256 _status; _status < uint256(type(IOracle.DisputeStatus).max); _status++) { if (IOracle.DisputeStatus(_status) == IOracle.DisputeStatus.Escalated) continue; - // Create a new dummy dispute - IOracle.Dispute memory _dispute = IOracle.Dispute({ - disputer: makeAddr('disputer'), - proposer: makeAddr('proposer'), - responseId: _getId(mockResponse), - requestId: _getId(_request) - }); - - // Check: does it revert if the dispute id is invalid? + mockDispute.requestId = _getId(_request); + bytes32 _disputeId = _getId(mockDispute); + + // Mock and expect IOracle.disputeStatus to be called + _mockAndExpect( + address(oracle), abi.encodeCall(oracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus(_status)) + ); + vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidDisputeId.selector)); vm.prank(address(oracle)); @@ -269,13 +274,13 @@ contract ArbitratorModule_Unit_ResolveDispute is BaseTest { /** * @notice Test that the resolve function reverts if the caller isn't the arbitrator */ - function test_revertIfWrongSender(bytes32 _disputeId, address _caller, IOracle.Request calldata _request) public { + function test_revertIfWrongSender(address _caller) public { vm.assume(_caller != address(oracle)); // Check: does it revert if not called by the Oracle? vm.expectRevert(IModule.Module_OnlyOracle.selector); vm.prank(_caller); - arbitratorModule.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + arbitratorModule.resolveDispute(_getId(mockDispute), mockRequest, mockResponse, mockDispute); } } From e80a97c9648d6aba25ee66a32d6977289c369590 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:31:45 +0400 Subject: [PATCH 37/53] feat: update `prophet-core` package --- package.json | 2 +- yarn.lock | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index dcb5ecad..d3eac0c9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "package.json": "sort-package-json" }, "dependencies": { - "@defi-wonderland/prophet-core-contracts": "0.0.0-a1d2cc55", + "@defi-wonderland/prophet-core-contracts": "0.0.0-1ae08a81", "@defi-wonderland/solidity-utils": "0.0.0-3e9c8e8b", "@openzeppelin/contracts": "^4.9.3", "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", diff --git a/yarn.lock b/yarn.lock index f3d82dcb..e3e8e0d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -192,10 +192,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@defi-wonderland/prophet-core-contracts@0.0.0-a1d2cc55": - version "0.0.0-a1d2cc55" - resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-a1d2cc55.tgz#e0bba63cdb143ffba6721049d2b0577eb39329fb" - integrity sha512-gl+8QvkzPd144yESzXhl2ceJ5blZczKh7HLioSfJ1uZnrJAR90z/MJD1fCb/1Q3oDyaq6WMWnJSK9VvMkadTtQ== +"@defi-wonderland/prophet-core-contracts@0.0.0-1ae08a81": + version "0.0.0-1ae08a81" + resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-1ae08a81.tgz#77d8f91303c8496556c0cedc12eeaa7abaa5f6fb" + integrity sha512-hMOBUsXR39NXfhExV3+xDBaUYv/6EYdyTrS/VMMSh0Gpbeqql4cIlUHnJzhmc6b+sbIgREq1K44v++8Aminrvw== dependencies: "@defi-wonderland/solidity-utils" "0.0.0-3e9c8e8b" "@openzeppelin/contracts" "^4.9.3" @@ -1181,11 +1181,15 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -"ds-test@git+https://github.com/dapphub/ds-test.git", "ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": +"ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" - uid e282159d5170298eb2455a6c05280ab5a73a4ef0 resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" +"ds-test@https://github.com/dapphub/ds-test": + version "1.0.0" + uid e282159d5170298eb2455a6c05280ab5a73a4ef0 + resolved "https://github.com/dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0" + "ds-test@https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" uid e282159d5170298eb2455a6c05280ab5a73a4ef0 @@ -1566,14 +1570,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" -"forge-std@git+https://github.com/foundry-rs/forge-std.git": - version "1.7.1" - resolved "git+https://github.com/foundry-rs/forge-std.git#37a37ab73364d6644bfe11edf88a07880f99bd56" - "forge-std@git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e": version "1.5.6" resolved "git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e" +"forge-std@https://github.com/foundry-rs/forge-std": + version "1.7.1" + resolved "https://github.com/foundry-rs/forge-std#37a37ab73364d6644bfe11edf88a07880f99bd56" + "forge-std@https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421": version "1.7.1" resolved "https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421" From 1d047f6e39ea326c497bf6440e4fc6dbac8fc7f4 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:33:11 +0400 Subject: [PATCH 38/53] test: index disputeId to fix a unit test --- solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol index 5cc4878a..2af65544 100644 --- a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol +++ b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol @@ -40,7 +40,7 @@ contract BaseTest is Test, Helpers { MockVerifier public mockVerifier; // Events - event DisputeStatusChanged(bytes32 _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); + event DisputeStatusChanged(bytes32 indexed _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); event ResponseDisputed( bytes32 indexed _requestId, bytes32 indexed _responseId, From 2dfa6c8b8e691e2d7b529924af4721393b3b382a Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:35:27 +0400 Subject: [PATCH 39/53] test: fix unit tests for `BondedDisputeModule` --- .../modules/dispute/BondedDisputeModule.sol | 3 +- .../modules/dispute/BondedDisputeModule.t.sol | 245 +++++++----------- 2 files changed, 89 insertions(+), 159 deletions(-) diff --git a/solidity/contracts/modules/dispute/BondedDisputeModule.sol b/solidity/contracts/modules/dispute/BondedDisputeModule.sol index bc03ebf9..1da2a6bd 100644 --- a/solidity/contracts/modules/dispute/BondedDisputeModule.sol +++ b/solidity/contracts/modules/dispute/BondedDisputeModule.sol @@ -38,8 +38,9 @@ contract BondedDisputeModule is Module, IBondedDisputeModule { emit ResponseDisputed({ _requestId: _dispute.requestId, _responseId: _dispute.responseId, + _disputeId: _getId(_dispute), _dispute: _dispute, - blockNumber: block.number + _blockNumber: block.number }); } diff --git a/solidity/test/unit/modules/dispute/BondedDisputeModule.t.sol b/solidity/test/unit/modules/dispute/BondedDisputeModule.t.sol index b02c87ef..cb93edb1 100644 --- a/solidity/test/unit/modules/dispute/BondedDisputeModule.t.sol +++ b/solidity/test/unit/modules/dispute/BondedDisputeModule.t.sol @@ -25,16 +25,6 @@ contract BaseTest is Test, Helpers { IAccountingExtension public accountingExtension; // A mock oracle IOracle public oracle; - // 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 dummy request - IOracle.Request public mockRequest; - // Create a dummy response - IOracle.Response public mockResponse; - // Create a dummy dispute - IOracle.Dispute public mockDispute; event DisputeStatusChanged(bytes32 indexed _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); // TODO: event ResponseDisputed(bytes32 indexed _requestId, bytes32 indexed _responseId, IOracle.Dispute _dispute, uint256 _blockNumber); @@ -50,8 +40,6 @@ contract BaseTest is Test, Helpers { vm.etch(address(accountingExtension), hex'069420'); bondedDisputeModule = new BondedDisputeModule(oracle); - - mockDispute = IOracle.Dispute({disputer: dude, proposer: dude, responseId: mockId, requestId: mockId}); } } @@ -60,7 +48,6 @@ contract BondedResponseModule_Unit_ModuleData is BaseTest { * @notice Test that the decodeRequestData function returns the correct values */ function test_decodeRequestData_returnsCorrectData( - bytes32 _requestId, address _accountingExtension, address _token, uint256 _bondSize @@ -68,9 +55,6 @@ contract BondedResponseModule_Unit_ModuleData is BaseTest { // Mock data bytes memory _requestData = abi.encode(_accountingExtension, _token, _bondSize); - // Store the mock request - // bondedDisputeModule.forTest_setRequestData(_requestId, _requestData); - // Test: decode the given request data IBondedDisputeModule.RequestParameters memory _storedParams = bondedDisputeModule.decodeRequestData(_requestData); @@ -90,183 +74,130 @@ contract BondedResponseModule_Unit_ModuleData is BaseTest { contract BondedResponseModule_Unit_OnDisputeStatusChange is BaseTest { /** - * @notice Test if onDisputeStatusChange correctly handle proposer or disputer win + * @notice Dispute lost by disputer */ - function test_correctWinnerPaid(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { - // Mock id's (insure they are different) - bytes32 _requestId = mockId; - bytes32 _responseId = bytes32(uint256(mockId) + 1); - - // Mock request data - bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); - - // Store the mock request - // bondedDisputeModule.forTest_setRequestData(mockId, _requestData); - - // ------------------------------------ - // Scenario: dispute won by proposer - // ------------------------------------ - - mockDispute = - IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); + function test_paysProposer(uint256 _bondSize, IERC20 _token) public { + mockRequest.disputeModuleData = + abi.encode(IBondedDisputeModule.RequestParameters(accountingExtension, _token, _bondSize)); + bytes32 _requestId = _getId(mockRequest); + mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); + + // Mock and expect IOracle.disputeStatus to be called + _mockAndExpect( + address(oracle), abi.encodeCall(oracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Lost) + ); - // Mock and expect the call to pay, from¨*proposer to disputer* + // Mock and expect the call to pay, from proposer to disputer _mockAndExpect( address(accountingExtension), - abi.encodeCall(accountingExtension.pay, (_requestId, _proposer, _disputer, _token, _bondSize)), + abi.encodeCall( + accountingExtension.pay, (_requestId, mockDispute.disputer, mockResponse.proposer, _token, _bondSize) + ), abi.encode() ); // Mock and expect the call to release, to the disputer _mockAndExpect( address(accountingExtension), - abi.encodeCall(accountingExtension.release, (_disputer, _requestId, _token, _bondSize)), + abi.encodeCall(accountingExtension.release, (mockResponse.proposer, _requestId, _token, _bondSize)), abi.encode() ); vm.prank(address(oracle)); - bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); - - // ------------------------------------ - // Scenario: dispute loss by proposer - // ------------------------------------ + bondedDisputeModule.onDisputeStatusChange(_getId(mockDispute), mockRequest, mockResponse, mockDispute); + } - mockDispute = - IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); + /** + * @notice Dispute won by disputer + */ + function test_paysDisputer(uint256 _bondSize, IERC20 _token) public { + mockRequest.disputeModuleData = + abi.encode(IBondedDisputeModule.RequestParameters(accountingExtension, _token, _bondSize)); + bytes32 _requestId = _getId(mockRequest); + mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); + + // Mock and expect IOracle.disputeStatus to be called + _mockAndExpect( + address(oracle), abi.encodeCall(oracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Won) + ); - // Mock and expect the call to pay, from *disputer to proposer* + // Mock and expect the call to pay, from disputer to proposer _mockAndExpect( address(accountingExtension), - abi.encodeCall(accountingExtension.pay, (_requestId, _disputer, _proposer, _token, _bondSize)), + abi.encodeCall( + accountingExtension.pay, (_requestId, mockResponse.proposer, mockDispute.disputer, _token, _bondSize) + ), abi.encode() ); // Mock and expect the call to release, for the proposer _mockAndExpect( address(accountingExtension), - abi.encodeCall(accountingExtension.release, (_proposer, _requestId, _token, _bondSize)), + abi.encodeCall(accountingExtension.release, (mockDispute.disputer, _requestId, _token, _bondSize)), abi.encode() ); vm.prank(address(oracle)); - bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); - - // ------------------------------------ - // Scenario: dispute with no resolution - // ------------------------------------ + bondedDisputeModule.onDisputeStatusChange(_getId(mockDispute), mockRequest, mockResponse, mockDispute); + } - mockDispute = - IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); + /** + * @notice Dispute with no resolution + */ + function test_refundsProposerAndDisputer(uint256 _bondSize, IERC20 _token) public { + mockRequest.disputeModuleData = + abi.encode(IBondedDisputeModule.RequestParameters(accountingExtension, _token, _bondSize)); + bytes32 _requestId = _getId(mockRequest); + mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); + + // Mock and expect IOracle.disputeStatus to be called + _mockAndExpect( + address(oracle), + abi.encodeCall(oracle.disputeStatus, (_disputeId)), + abi.encode(IOracle.DisputeStatus.NoResolution) + ); // Mock and expect the call to release, for the proposer _mockAndExpect( address(accountingExtension), - abi.encodeCall(accountingExtension.release, (_proposer, _requestId, _token, _bondSize)), + abi.encodeCall(accountingExtension.release, (mockResponse.proposer, _requestId, _token, _bondSize)), abi.encode() ); // Mock and expect the call to release, for the disputer _mockAndExpect( address(accountingExtension), - abi.encodeCall(accountingExtension.release, (_disputer, _requestId, _token, _bondSize)), + abi.encodeCall(accountingExtension.release, (mockDispute.disputer, _requestId, _token, _bondSize)), abi.encode() ); vm.prank(address(oracle)); - bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); - } - - function test_statusWithNoChange(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { - // Mock id's (insure they are different) - bytes32 _requestId = mockId; - bytes32 _responseId = bytes32(uint256(mockId) + 1); - - // Mock request data - bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); - - // Store the mock request - // bondedDisputeModule.forTest_setRequestData(mockId, _requestData); - - // ------------------------------------ - // Scenario: dispute new status is None - // ------------------------------------ - - mockDispute = - IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); - - // Expect the event - vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); - emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.None); - - vm.prank(address(oracle)); - bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); - - // ------------------------------------ - // Scenario: dispute new status is Active - // ------------------------------------ - - mockDispute = - IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); - - // Expect the event - vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); - emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.Active); - - vm.prank(address(oracle)); - bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); - // ------------------------------------ - // Scenario: dispute new status is Escalated - // ------------------------------------ - - mockDispute = - IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); - - // Expect the event - vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); - emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.Escalated); - - vm.prank(address(oracle)); - bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); + bondedDisputeModule.onDisputeStatusChange(_getId(mockDispute), mockRequest, mockResponse, mockDispute); } - function test_emitsEvent(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { - // Mock id's (insure they are different) - bytes32 _requestId = mockId; - bytes32 _responseId = bytes32(uint256(mockId) + 1); - + function test_statusWithNoChange(uint256 _bondSize, IERC20 _token) public { // Mock request data - bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); - - // Store the mock request - // bondedDisputeModule.forTest_setRequestData(mockId, _requestData); - - // ------------------------------------ - // Scenario: dispute won by proposer - // ------------------------------------ - - mockDispute = - IOracle.Dispute({disputer: _disputer, proposer: _proposer, responseId: _responseId, requestId: _requestId}); - - // Mock and expect the call to pay, from¨*proposer to disputer* - _mockAndExpect( - address(accountingExtension), - abi.encodeCall(accountingExtension.pay, (_requestId, _proposer, _disputer, _token, _bondSize)), - abi.encode() - ); - - // Mock and expect the call to release, to the disputer - _mockAndExpect( - address(accountingExtension), - abi.encodeCall(accountingExtension.release, (_disputer, _requestId, _token, _bondSize)), - abi.encode() - ); - - // Expect the event - vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); - emit DisputeStatusChanged(_requestId, _responseId, _disputer, _proposer, IOracle.DisputeStatus.Won); - - vm.prank(address(oracle)); - bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); + mockRequest.disputeModuleData = + abi.encode(IBondedDisputeModule.RequestParameters(accountingExtension, _token, _bondSize)); + mockDispute.requestId = _getId(mockRequest); + bytes32 _disputeId = _getId(mockDispute); + + for (uint256 _status; _status < 1; _status++) { + // Mock and expect IOracle.disputeStatus to be called + _mockAndExpect( + address(oracle), abi.encodeCall(oracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus(_status)) + ); + + // Expect the event + vm.expectEmit(true, true, true, true, address(bondedDisputeModule)); + emit DisputeStatusChanged(_disputeId, mockDispute, IOracle.DisputeStatus(_status)); + + vm.prank(address(oracle)); + bondedDisputeModule.onDisputeStatusChange(_disputeId, mockRequest, mockResponse, mockDispute); + } } /** @@ -280,7 +211,7 @@ contract BondedResponseModule_Unit_OnDisputeStatusChange is BaseTest { // Test: call disputeResponse from non-oracle address vm.prank(_randomCaller); - bondedDisputeModule.onDisputeStatusChange(mockId, mockRequest, mockResponse, mockDispute); + bondedDisputeModule.onDisputeStatusChange(_getId(mockDispute), mockRequest, mockResponse, mockDispute); } } @@ -288,21 +219,19 @@ contract BondedResponseModule_Unit_DisputeResponse is BaseTest { /** * @notice Test if dispute response returns the correct status */ - function test_createBond(uint256 _bondSize, address _disputer, address _proposer, IERC20 _token) public { - // Mock id's (insure they are different) - bytes32 _requestId = mockId; - bytes32 _responseId = bytes32(uint256(mockId) + 1); - + function test_createBond(uint256 _bondSize, IERC20 _token) public { // Mock request data - bytes memory _requestData = abi.encode(accountingExtension, _token, _bondSize); - - // Store the mock request - // bondedDisputeModule.forTest_setRequestData(mockId, _requestData); + mockRequest.disputeModuleData = + abi.encode(IBondedDisputeModule.RequestParameters(accountingExtension, _token, _bondSize)); + bytes32 _requestId = _getId(mockRequest); + mockDispute.requestId = _requestId; // Mock and expect the call to the accounting extension, initiating the bond _mockAndExpect( address(accountingExtension), - abi.encodeWithSignature('bond(address,bytes32,address,uint256)', _disputer, _requestId, _token, _bondSize), + abi.encodeWithSignature( + 'bond(address,bytes32,address,uint256)', mockDispute.disputer, _requestId, _token, _bondSize + ), abi.encode() ); From 2ff94bde241bd9d718421bae5f15bd77c29ad980 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:36:36 +0400 Subject: [PATCH 40/53] docs: tiny natspec update --- .../interfaces/modules/resolution/IArbitratorModule.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/solidity/interfaces/modules/resolution/IArbitratorModule.sol b/solidity/interfaces/modules/resolution/IArbitratorModule.sol index 3582fa54..fe78357f 100644 --- a/solidity/interfaces/modules/resolution/IArbitratorModule.sol +++ b/solidity/interfaces/modules/resolution/IArbitratorModule.sol @@ -76,8 +76,8 @@ interface IArbitratorModule is IResolutionModule { /** * @notice Starts the arbitration process by calling `resolve` on the arbitrator and flags the dispute as Active * - * @dev Only callable by the Oracle - * @dev Will revert if the arbitrator address is the address zero + * @dev Only callable by the Oracle + * @dev Will revert if the arbitrator address is the address zero * @param _disputeId The ID of the dispute * @param _request The request * @param _response The disputed response @@ -93,7 +93,7 @@ interface IArbitratorModule is IResolutionModule { /** * @notice Resolves the dispute by getting the answer from the arbitrator and updating the dispute status * - * @dev Only callable by the Oracle + * @dev Only callable by the Oracle * @param _disputeId The ID of the dispute * @param _request The request * @param _response The disputed response From de381c6825f620e91b3bcd4b6cbf036c271e1342 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:49:42 +0400 Subject: [PATCH 41/53] test: improve unit tests --- .../finality/MultipleCallbacksModule.t.sol | 59 +++++++------------ 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol b/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol index be721d19..55c93426 100644 --- a/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol +++ b/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol @@ -18,18 +18,9 @@ contract BaseTest is Test, Helpers { MultipleCallbacksModule public multipleCallbackModule; // A mock oracle IOracle public oracle; - // Mock EOA proposer - address public proposer = makeAddr('proposer'); - // Mock EOA disputer - address public disputer = makeAddr('disputer'); - // Create a new dummy dispute - IOracle.Dispute public mockDispute; - // Create a new dummy response - IOracle.Response public mockResponse; - bytes32 public mockId = bytes32('69'); + // Events event Callback(bytes32 indexed _request, address indexed _target, bytes _data); - event RequestFinalized(bytes32 indexed _requestId, address _finalizer); /** * @notice Deploy the target and mock oracle+accounting extension @@ -39,9 +30,6 @@ contract BaseTest is Test, Helpers { vm.etch(address(oracle), hex'069420'); multipleCallbackModule = new MultipleCallbacksModule(oracle); - mockDispute = - IOracle.Dispute({disputer: disputer, proposer: proposer, responseId: bytes32('69'), requestId: bytes32('69')}); - mockResponse = IOracle.Response({proposer: proposer, requestId: mockId, response: bytes('')}); } } @@ -61,36 +49,33 @@ contract MultipleCallbacksModule_Unit_FinalizeRequests is BaseTest { /** * @notice Test that finalizeRequests calls the _target.callback with the correct data */ - function test_finalizeRequest( - IOracle.Request calldata _request, - address[1] calldata _targets, - bytes[1] calldata __data - ) public { - bytes32 _requestId = _getId(_request); - address _target = _targets[0]; - bytes calldata _data = __data[0]; - - assumeNotPrecompile(_target); - vm.assume(_target != address(vm)); - - // Create and set some mock request data - address[] memory _targetParams = new address[](1); - _targetParams[0] = _targets[0]; - bytes[] memory _dataParams = new bytes[](1); - _dataParams[0] = __data[0]; - - _mockAndExpect(_target, _data, abi.encode()); + function test_finalizeRequest(address[] calldata _targets, bytes[] calldata _data) public { + vm.assume(_targets.length == _data.length); - // Check: is the event emitted? - vm.expectEmit(true, true, true, true, address(multipleCallbackModule)); - emit Callback(_requestId, _target, _data); + mockRequest.finalityModuleData = + abi.encode(IMultipleCallbacksModule.RequestParameters({targets: _targets, data: _data})); + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + + for (uint256 _i; _i < _targets.length; _i++) { + address _target = _targets[_i]; + bytes calldata _calldata = _data[_i]; + + // Skip precompiles, VM, console.log addresses, etc + _assumeFuzzable(_target); + _mockAndExpect(_target, _calldata, abi.encode()); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(multipleCallbackModule)); + emit Callback(_requestId, _target, _calldata); + } // Check: is the event emitted? vm.expectEmit(true, true, true, true, address(multipleCallbackModule)); - emit RequestFinalized(_requestId, address(oracle)); + emit RequestFinalized(_requestId, mockResponse, address(oracle)); vm.prank(address(oracle)); - multipleCallbackModule.finalizeRequest(_request, mockResponse, address(oracle)); + multipleCallbackModule.finalizeRequest(mockRequest, mockResponse, address(oracle)); } /** From 53e41f4d54b037b5a26a6b903db4f3d01deb0320 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:50:54 +0400 Subject: [PATCH 42/53] docs: natspec fixes --- .../modules/finality/multiple_callbacks_module.md | 2 +- .../modules/finality/IMultipleCallbacksModule.sol | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/src/content/modules/finality/multiple_callbacks_module.md b/docs/src/content/modules/finality/multiple_callbacks_module.md index a1bef269..1b4134a3 100644 --- a/docs/src/content/modules/finality/multiple_callbacks_module.md +++ b/docs/src/content/modules/finality/multiple_callbacks_module.md @@ -10,7 +10,7 @@ The `MultipleCallbacksModule` is a finality module that allows users to make mul ### Key Methods -- `decodeRequestData(bytes calldata _data)`: Returns the decoded data for a request. The returned data includes the target addresses for the callback and the calldata forwarded to the targets. +- `decodeRequestData`: Returns the decoded data for a request. The returned data includes the target addresses for the callback and the calldata forwarded to the targets. - `finalizeRequest`: Finalizes the request by executing the callback calls on the targets. ### Request Parameters diff --git a/solidity/interfaces/modules/finality/IMultipleCallbacksModule.sol b/solidity/interfaces/modules/finality/IMultipleCallbacksModule.sol index c9c0b456..ea4546b3 100644 --- a/solidity/interfaces/modules/finality/IMultipleCallbacksModule.sol +++ b/solidity/interfaces/modules/finality/IMultipleCallbacksModule.sol @@ -36,23 +36,26 @@ interface IMultipleCallbacksModule is IFinalityModule { address[] targets; bytes[] data; } + /*/////////////////////////////////////////////////////////////// LOGIC //////////////////////////////////////////////////////////////*/ /** * @notice Returns the decoded data for a request - * @param _data The encoded request parameters - * @return _params The struct containing the parameters for the 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 the request by executing the callback calls on the targets - * @dev The success of the callback calls is purposely not checked - * @param _request The request being finalized - * @param _response The response - * @param _finalizer The address finalizing the request + * + * @dev The success of the callback calls is purposely not checked + * @param _request The request being finalized + * @param _response The response + * @param _finalizer The address finalizing the request */ function finalizeRequest( IOracle.Request calldata _request, From 3f95e872277f6c93853cd168e2f71db6a5f206f3 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:33:11 +0400 Subject: [PATCH 43/53] test: index disputeId to fix a unit test --- solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol index 5cc4878a..2af65544 100644 --- a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol +++ b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol @@ -40,7 +40,7 @@ contract BaseTest is Test, Helpers { MockVerifier public mockVerifier; // Events - event DisputeStatusChanged(bytes32 _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); + event DisputeStatusChanged(bytes32 indexed _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); event ResponseDisputed( bytes32 indexed _requestId, bytes32 indexed _responseId, From cde7b881525252116f2df90723cf90804cc874e0 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 20:16:31 +0400 Subject: [PATCH 44/53] test: unit tests --- .../SparseMerkleTreeRequestModule.t.sol | 111 ++++++------------ 1 file changed, 38 insertions(+), 73 deletions(-) diff --git a/solidity/test/unit/modules/request/SparseMerkleTreeRequestModule.t.sol b/solidity/test/unit/modules/request/SparseMerkleTreeRequestModule.t.sol index 051a1e3d..f9067ac0 100644 --- a/solidity/test/unit/modules/request/SparseMerkleTreeRequestModule.t.sol +++ b/solidity/test/unit/modules/request/SparseMerkleTreeRequestModule.t.sol @@ -25,18 +25,6 @@ contract BaseTest is Test, Helpers { SparseMerkleTreeRequestModule public sparseMerkleTreeRequestModule; // A mock oracle IOracle public oracle; - // A mock accounting extension - IAccountingExtension public accounting; - // A mock tree verifier - ITreeVerifier public treeVerifier; - // Create a new dummy response - IOracle.Response public mockResponse; - // Create a new dummy dispute - IOracle.Dispute public mockDispute; - // 100% random sequence of bytes representing request, response, or dispute id - bytes32 public mockId = bytes32('69'); - address internal _disputer = makeAddr('disputer'); - address internal _proposer = makeAddr('proposer'); // Mock data for the request bytes32[32] internal _treeBranches = [ @@ -77,8 +65,6 @@ contract BaseTest is Test, Helpers { bytes internal _treeData = abi.encode(_treeBranches, _treeCount); bytes32[] internal _leavesToInsert = [bytes32('leave1'), bytes32('leave2')]; - event RequestFinalized(bytes32 indexed _requestId, address _finalizer); - /** * @notice Deploy the target and mock oracle+accounting extension */ @@ -86,15 +72,7 @@ contract BaseTest is Test, Helpers { oracle = IOracle(makeAddr('Oracle')); vm.etch(address(oracle), hex'069420'); - accounting = IAccountingExtension(makeAddr('AccountingExtension')); - vm.etch(address(accounting), hex'069420'); - treeVerifier = ITreeVerifier(makeAddr('TreeVerifier')); - vm.etch(address(accounting), hex'069420'); - sparseMerkleTreeRequestModule = new SparseMerkleTreeRequestModule(oracle); - - mockDispute = IOracle.Dispute({disputer: _disputer, responseId: mockId, proposer: _proposer, requestId: mockId}); - mockResponse = IOracle.Response({proposer: _proposer, requestId: mockId, response: bytes('')}); } } @@ -110,7 +88,6 @@ contract SparseMerkleTreeRequestModule_Unit_ModuleData is BaseTest { * @notice Test that the decodeRequestData function returns the correct values */ function test_decodeRequestData( - bytes32 _requestId, IERC20 _paymentToken, uint256 _paymentAmount, IAccountingExtension _accounting, @@ -151,24 +128,16 @@ contract SparseMerkleTreeRequestModule_Unit_ModuleData is BaseTest { contract SparseMerkleTreeRequestModule_Unit_FinalizeRequest is BaseTest { /** - * @notice Test that finalizeRequest calls: - * - oracle get request - * - oracle get response - * - accounting extension pay - * - accounting extension release + * @notice Test that the proposer gets paid for a correct response */ - function test_makesCalls( - bytes32 _requestId, - address _requester, - address _proposer, + function test_paysProposer( IERC20 _paymentToken, uint256 _paymentAmount, IAccountingExtension _accounting, - ITreeVerifier _treeVerifier, - IOracle.Request calldata _request + ITreeVerifier _treeVerifier ) public assumeFuzzable(address(_accounting)) { // Use the correct accounting parameters - bytes memory _requestData = abi.encode( + mockRequest.requestModuleData = abi.encode( ISparseMerkleTreeRequestModule.RequestParameters({ treeData: _treeData, leavesToInsert: _leavesToInsert, @@ -179,44 +148,43 @@ contract SparseMerkleTreeRequestModule_Unit_FinalizeRequest is BaseTest { }) ); - IOracle.Request memory _fullRequest; - _fullRequest.requester = _requester; + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; - IOracle.Response memory _fullResponse; - _fullResponse.proposer = _proposer; + // Oracle confirms that the response has been created + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.createdAt, (_getId(mockResponse))), abi.encode(block.timestamp) + ); // Mock and expect IAccountingExtension.pay to be called - vm.mockCall( + _mockAndExpect( address(_accounting), - abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _paymentToken, _paymentAmount)), + abi.encodeCall( + IAccountingExtension.pay, + (_requestId, mockRequest.requester, mockResponse.proposer, _paymentToken, _paymentAmount) + ), abi.encode() ); - vm.startPrank(address(oracle)); - sparseMerkleTreeRequestModule.finalizeRequest(_request, mockResponse, address(oracle)); - - // 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(sparseMerkleTreeRequestModule)); + emit RequestFinalized(_requestId, mockResponse, address(this)); - sparseMerkleTreeRequestModule.finalizeRequest(_request, mockResponse, address(this)); + vm.prank(address(oracle)); + sparseMerkleTreeRequestModule.finalizeRequest(mockRequest, mockResponse, address(this)); } - function test_emitsEvent( - bytes32 _requestId, - address _requester, - address _proposer, + /** + * @notice Test that the requester gets a refund in case of no responses + */ + function test_refundsRequester( IERC20 _paymentToken, uint256 _paymentAmount, IAccountingExtension _accounting, - ITreeVerifier _treeVerifier, - IOracle.Request calldata _request + ITreeVerifier _treeVerifier ) public assumeFuzzable(address(_accounting)) { // Use the correct accounting parameters - bytes memory _requestData = abi.encode( + mockRequest.requestModuleData = abi.encode( ISparseMerkleTreeRequestModule.RequestParameters({ treeData: _treeData, leavesToInsert: _leavesToInsert, @@ -227,40 +195,37 @@ contract SparseMerkleTreeRequestModule_Unit_FinalizeRequest is BaseTest { }) ); - // Mock and expect pay to be called - _mockAndExpect( - address(_accounting), - abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _paymentToken, _paymentAmount)), - abi.encode() - ); + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; - vm.startPrank(address(oracle)); - sparseMerkleTreeRequestModule.finalizeRequest(_request, mockResponse, address(oracle)); + // Oracle returns no createdAt value - finalizing without a response + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.createdAt, (_getId(mockResponse))), abi.encode(0)); - // Mock and expect release to be called + // Mock and expect IAccountingExtension.release to be called _mockAndExpect( address(_accounting), - abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _paymentToken, _paymentAmount)), - abi.encode(true) + abi.encodeCall(IAccountingExtension.release, (mockRequest.requester, _requestId, _paymentToken, _paymentAmount)), + abi.encode() ); // Check: is the event emitted? vm.expectEmit(true, true, true, true, address(sparseMerkleTreeRequestModule)); - emit RequestFinalized(_requestId, address(this)); + emit RequestFinalized(_requestId, mockResponse, address(this)); - sparseMerkleTreeRequestModule.finalizeRequest(_request, mockResponse, address(this)); + vm.prank(address(oracle)); + sparseMerkleTreeRequestModule.finalizeRequest(mockRequest, mockResponse, address(this)); } /** * @notice Test that the finalizeRequest reverts if caller is not the oracle */ - function test_revertsIfWrongCaller(IOracle.Request calldata _request, address _caller) public { + function test_revertsIfWrongCaller(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); - sparseMerkleTreeRequestModule.finalizeRequest(_request, mockResponse, address(_caller)); + sparseMerkleTreeRequestModule.finalizeRequest(mockRequest, mockResponse, address(_caller)); } } From 973c9f0dad6af54a1e87a74908c669527cafd1be Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:33:11 +0400 Subject: [PATCH 45/53] test: index disputeId to fix a unit test --- solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol index 5cc4878a..2af65544 100644 --- a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol +++ b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol @@ -40,7 +40,7 @@ contract BaseTest is Test, Helpers { MockVerifier public mockVerifier; // Events - event DisputeStatusChanged(bytes32 _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); + event DisputeStatusChanged(bytes32 indexed _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); event ResponseDisputed( bytes32 indexed _requestId, bytes32 indexed _responseId, From 8103280d188470f7787585e72f81b0999123d416 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 20:19:44 +0400 Subject: [PATCH 46/53] fix: `onlyOracle` --- .../contracts/modules/request/SparseMerkleTreeRequestModule.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/contracts/modules/request/SparseMerkleTreeRequestModule.sol b/solidity/contracts/modules/request/SparseMerkleTreeRequestModule.sol index 899f2958..b5ba9938 100644 --- a/solidity/contracts/modules/request/SparseMerkleTreeRequestModule.sol +++ b/solidity/contracts/modules/request/SparseMerkleTreeRequestModule.sol @@ -21,7 +21,7 @@ contract SparseMerkleTreeRequestModule is Module, ISparseMerkleTreeRequestModule } /// @inheritdoc ISparseMerkleTreeRequestModule - function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external { + function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external onlyOracle { RequestParameters memory _params = decodeRequestData(_data); _params.accountingExtension.bond({ From 5ed765165f9f065a21f83e4a470b5cd32b55781e Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 20:21:09 +0400 Subject: [PATCH 47/53] docs: review natspec and documentation --- .../sparse_merkle_tree_request_module.md | 6 +++--- .../ISparseMerkleTreeRequestModule.sol | 19 +++++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/src/content/modules/request/sparse_merkle_tree_request_module.md b/docs/src/content/modules/request/sparse_merkle_tree_request_module.md index 44fc3060..3968ba90 100644 --- a/docs/src/content/modules/request/sparse_merkle_tree_request_module.md +++ b/docs/src/content/modules/request/sparse_merkle_tree_request_module.md @@ -10,8 +10,8 @@ The `SparseMerkleTreeRequestModule` is a contract that allows a user to request ### Key Methods -- `decodeRequestData(bytes calldata _data)`: This function decodes the request data for a given request ID. It returns a RequestParameters struct that contains the parameters for the request. -- `finalizeRequest(bytes32 _requestId, address _finalizer)`: This function is called by the Oracle to finalize the request. It either pays the proposer for the response or releases the requester's bond if no response was submitted. +- `decodeRequestData`: This function decodes the request data for a given request ID. It returns a RequestParameters struct that contains the parameters for the request. +- `finalizeRequest`: This function is called by the Oracle to finalize the request. It either pays the proposer for the response or releases the requester's bond if no response was submitted. ### Request Parameters @@ -28,5 +28,5 @@ The `SparseMerkleTreeRequestModule` uses a Merkle tree to calculate the root fro ## 4. Gotchas -- The module is supposed to be paired with the root verification module. +- The module is supposed to be paired with [`RootVerificationModule`](../dispute/root_verification_module.md). - The verifier contract must follow the `ITreeVerifier` interface, otherwise the proposers won't be able to calculate the correct response. diff --git a/solidity/interfaces/modules/request/ISparseMerkleTreeRequestModule.sol b/solidity/interfaces/modules/request/ISparseMerkleTreeRequestModule.sol index 8357ee8e..7add6179 100644 --- a/solidity/interfaces/modules/request/ISparseMerkleTreeRequestModule.sol +++ b/solidity/interfaces/modules/request/ISparseMerkleTreeRequestModule.sol @@ -43,20 +43,27 @@ interface ISparseMerkleTreeRequestModule is IRequestModule { /** * @notice Returns the decoded data for a request - * @param _data The encoded request parameters - * @return _params The struct containing the parameters for the 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); - /// @inheritdoc IRequestModule + /** + * @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 + * @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, From a29dfab47bdfa33d0c4e0ac956350c6b81ca3ae9 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 21:52:29 +0400 Subject: [PATCH 48/53] test: fix fuzzing issues --- .../modules/finality/MultipleCallbacksModule.t.sol | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol b/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol index 55c93426..376cbe44 100644 --- a/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol +++ b/solidity/test/unit/modules/finality/MultipleCallbacksModule.t.sol @@ -49,8 +49,15 @@ contract MultipleCallbacksModule_Unit_FinalizeRequests is BaseTest { /** * @notice Test that finalizeRequests calls the _target.callback with the correct data */ - function test_finalizeRequest(address[] calldata _targets, bytes[] calldata _data) public { - vm.assume(_targets.length == _data.length); + function test_finalizeRequest(address[10] calldata _fuzzedTargets, bytes[10] calldata _fuzzedData) public { + address[] memory _targets = new address[](_fuzzedTargets.length); + bytes[] memory _data = new bytes[](_fuzzedTargets.length); + + // Copying the values to fresh arrays that we can use to build `RequestParameters` + for (uint256 _i; _i < _fuzzedTargets.length; _i++) { + _targets[_i] = _fuzzedTargets[_i]; + _data[_i] = _fuzzedData[_i]; + } mockRequest.finalityModuleData = abi.encode(IMultipleCallbacksModule.RequestParameters({targets: _targets, data: _data})); @@ -59,7 +66,7 @@ contract MultipleCallbacksModule_Unit_FinalizeRequests is BaseTest { for (uint256 _i; _i < _targets.length; _i++) { address _target = _targets[_i]; - bytes calldata _calldata = _data[_i]; + bytes memory _calldata = _data[_i]; // Skip precompiles, VM, console.log addresses, etc _assumeFuzzable(_target); From 580f9cabb909cfde1172c3a1013b213ae76ffe03 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:33:11 +0400 Subject: [PATCH 49/53] test: index disputeId to fix a unit test --- solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol index 5cc4878a..2af65544 100644 --- a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol +++ b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol @@ -40,7 +40,7 @@ contract BaseTest is Test, Helpers { MockVerifier public mockVerifier; // Events - event DisputeStatusChanged(bytes32 _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); + event DisputeStatusChanged(bytes32 indexed _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); event ResponseDisputed( bytes32 indexed _requestId, bytes32 indexed _responseId, From d6daaec820681fe0d7ae48cf4b388c7a9a5dcbce Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sun, 12 Nov 2023 00:55:26 +0400 Subject: [PATCH 50/53] docs: revise natspec and documentation --- .../resolution/erc20_resolution_module.md | 10 ++--- .../resolution/ERC20ResolutionModule.sol | 5 ++- .../resolution/IERC20ResolutionModule.sol | 45 ++++++++++++++----- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/docs/src/content/modules/resolution/erc20_resolution_module.md b/docs/src/content/modules/resolution/erc20_resolution_module.md index 3905c8fb..6be220b3 100644 --- a/docs/src/content/modules/resolution/erc20_resolution_module.md +++ b/docs/src/content/modules/resolution/erc20_resolution_module.md @@ -10,11 +10,11 @@ The `ERC20ResolutionModule` is a dispute resolution module that decides on the o ### Key Methods -- `decodeRequestData(bytes calldata _data)`: Decodes the request data associated with a given request ID. -- `startResolution(bytes32 _disputeId)`: Starts the resolution process for a given dispute. -- `castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes)`: Allows a user to cast votes for a dispute. -- `resolveDispute(bytes32 _disputeId)`: Resolves a dispute based on the votes cast. -- `getVoters(bytes32 _disputeId)`: Returns the addresses of the voters for a given dispute. +- `decodeRequestData`: Decodes the request data associated with a given request ID. +- `startResolution`: Starts the resolution process for a given dispute. +- `castVote`: Allows a user to cast votes for a dispute. +- `resolveDispute`: Resolves a dispute based on the votes cast. +- `getVoters`: Returns the addresses of the voters for a given dispute. ### Request Parameters diff --git a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol index dfd9197b..6ffa6791 100644 --- a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol +++ b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol @@ -22,7 +22,10 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule { /// @inheritdoc IERC20ResolutionModule mapping(bytes32 _disputeId => mapping(address _voter => uint256 _numOfVotes)) public votes; - mapping(bytes32 _disputeId => EnumerableSet.AddressSet _votersSet) private _voters; + /** + * @notice The list of voters for each dispute + */ + mapping(bytes32 _disputeId => EnumerableSet.AddressSet _votersSet) internal _voters; constructor(IOracle _oracle) Module(_oracle) {} diff --git a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol index 58cb623b..38995b4c 100644 --- a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol +++ b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol @@ -105,22 +105,36 @@ interface IERC20ResolutionModule is IResolutionModule { /** * @notice Returns the escalation data for a dispute - * @param _disputeId The id of the dispute - * @return _startTime The timestamp at which the dispute was escalated - * @return _totalVotes The total amount of votes cast for the dispute + * + * @param _disputeId The id of the dispute + * @return _startTime The timestamp at which the dispute was escalated + * @return _totalVotes The total amount of votes cast for the dispute */ function escalations(bytes32 _disputeId) external view returns (uint256 _startTime, uint256 _totalVotes); + /** + * @notice Returns the amount of votes the given voter has cast for the given dispute + * + * @param _disputeId The id of the dispute + * @param _voter The address of the voter + * @return _votes The amount of votes the voter has cast + */ function votes(bytes32 _disputeId, address _voter) external view returns (uint256 _votes); /** * @notice Returns the decoded data for a request - * @param _data The encoded request parameters - * @return _params The struct containing the parameters for the 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); - /// @inheritdoc IResolutionModule + /** + * @notice Casts a vote in favor of a dispute + * + * @param _request The request for which the dispute was created + * @param _dispute The dispute for which the vote is being cast + */ function startResolution( bytes32 _disputeId, IOracle.Request calldata _request, @@ -130,7 +144,10 @@ interface IERC20ResolutionModule is IResolutionModule { /** * @notice Casts a vote in favor of a dispute - * @param _numberOfVotes The number of votes to cast + * + * @param _request The request for which the dispute was created + * @param _dispute The dispute for which the vote is being cast + * @param _numberOfVotes The number of votes to cast */ function castVote( IOracle.Request calldata _request, @@ -138,7 +155,14 @@ interface IERC20ResolutionModule is IResolutionModule { uint256 _numberOfVotes ) external; - /// @inheritdoc IResolutionModule + /** + * @notice Settles the dispute, transferring the funds back to the voters + * + * @param _disputeId The id of the dispute + * @param _request The request for which the dispute was created + * @param _response The disputed response + * @param _dispute The dispute that is being resolved + */ function resolveDispute( bytes32 _disputeId, IOracle.Request calldata _request, @@ -148,8 +172,9 @@ interface IERC20ResolutionModule is IResolutionModule { /** * @notice Gets the voters of a dispute - * @param _disputeId The id of the dispute - * @return _voters The addresses of the voters + * + * @param _disputeId The id of the dispute + * @return _voters The addresses of the voters */ function getVoters(bytes32 _disputeId) external view returns (address[] memory _voters); } From 02a871bfc2846e31769012cf031fae013f90a842 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sun, 12 Nov 2023 00:57:19 +0400 Subject: [PATCH 51/53] feat: simplify checks, reducing external calls --- .../modules/resolution/ERC20ResolutionModule.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol index 6ffa6791..ae1cf75e 100644 --- a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol +++ b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol @@ -57,11 +57,11 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule { uint256 _numberOfVotes ) public { bytes32 _disputeId = _getId(_dispute); - if (ORACLE.createdAt(_disputeId) == 0) revert ERC20ResolutionModule_NonExistentDispute(); - if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.None) revert ERC20ResolutionModule_AlreadyResolved(); - Escalation memory _escalation = escalations[_disputeId]; if (_escalation.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated(); + if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.Escalated) { + revert ERC20ResolutionModule_AlreadyResolved(); + } RequestParameters memory _params = decodeRequestData(_request.resolutionModuleData); uint256 _deadline = _escalation.startTime + _params.timeUntilDeadline; @@ -84,11 +84,12 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule { IOracle.Dispute calldata _dispute ) external onlyOracle { // 0. Check disputeId actually exists and that it isn't resolved already - if (ORACLE.createdAt(_disputeId) == 0) revert ERC20ResolutionModule_NonExistentDispute(); - if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.None) revert ERC20ResolutionModule_AlreadyResolved(); + if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.Escalated) { + revert ERC20ResolutionModule_AlreadyResolved(); + } - Escalation memory _escalation = escalations[_disputeId]; // 1. Check that the dispute is actually escalated + Escalation memory _escalation = escalations[_disputeId]; if (_escalation.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated(); // 2. Check that voting deadline is over From 1c39fab5edebcc3ff665aea87a3f121acda69040 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sun, 12 Nov 2023 00:57:39 +0400 Subject: [PATCH 52/53] refactor: remove an unused error --- .../modules/resolution/IERC20ResolutionModule.sol | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol index 38995b4c..95cbe46f 100644 --- a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol +++ b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol @@ -44,7 +44,7 @@ interface IERC20ResolutionModule is IResolutionModule { error ERC20ResolutionModule_OnlyDisputeModule(); /** - * @notice Throws if the dispute has not been escalated + * @notice Throws if the dispute doesn't exist or has not been escalated */ error ERC20ResolutionModule_DisputeNotEscalated(); @@ -63,11 +63,6 @@ interface IERC20ResolutionModule is IResolutionModule { */ error ERC20ResolutionModule_OnGoingVotingPhase(); - /** - * @notice Throws if the dispute does not exist - */ - error ERC20ResolutionModule_NonExistentDispute(); - /** * @notice Throws if the dispute has already been resolved */ From 0aec25baaaf0310f93801665c3d337853f05d067 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sun, 12 Nov 2023 00:58:35 +0400 Subject: [PATCH 53/53] test: fix unit tests --- .../resolution/ERC20ResolutionModule.t.sol | 284 ++++++++---------- 1 file changed, 131 insertions(+), 153 deletions(-) diff --git a/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol b/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol index dc87ae63..304a55b3 100644 --- a/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol +++ b/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol @@ -5,7 +5,9 @@ import 'forge-std/Test.sol'; import {Helpers} from '../../../utils/Helpers.sol'; +import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.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'; @@ -15,15 +17,26 @@ import { } from '../../../../contracts/modules/resolution/ERC20ResolutionModule.sol'; contract ForTest_ERC20ResolutionModule is ERC20ResolutionModule { + using EnumerableSet for EnumerableSet.AddressSet; + constructor(IOracle _oracle) ERC20ResolutionModule(_oracle) {} - function forTest_setEscalation(bytes32 _disputeId, ERC20ResolutionModule.Escalation calldata __escalation) public { - escalations[_disputeId] = __escalation; + function forTest_setStartTime(bytes32 _disputeId, uint256 _startTime) public { + escalations[_disputeId] = IERC20ResolutionModule.Escalation({ + startTime: _startTime, + totalVotes: 0 // Initial amount of votes + }); } function forTest_setVotes(bytes32 _disputeId, address _voter, uint256 _amountOfVotes) public { votes[_disputeId][_voter] = _amountOfVotes; } + + function forTest_castVote(bytes32 _disputeId, address _voter, uint256 _numberOfVotes) public { + votes[_disputeId][_voter] += _numberOfVotes; + _voters[_disputeId].add(_voter); + escalations[_disputeId].totalVotes += _numberOfVotes; + } } contract BaseTest is Test, Helpers { @@ -33,18 +46,10 @@ contract BaseTest is Test, Helpers { IOracle public oracle; // A mock token IERC20 public token; - // Mock EOA proposer - address public proposer = makeAddr('proposer'); - // Mock EOA disputer - address public disputer = makeAddr('disputer'); - // Create a new dummy dispute - IOracle.Dispute public mockDispute; - // Create a new dummy response - IOracle.Response public mockResponse; - address internal _proposer = makeAddr('proposer'); - bytes32 public mockId = bytes32('69'); - - // Mocking module events + + uint256 public votingTimeWindow = 40_000; + + // Events event VoteCast(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); event VotingPhaseStarted(uint256 _startTime, bytes32 _disputeId); event DisputeResolved(bytes32 indexed _requestId, bytes32 indexed _disputeId, IOracle.DisputeStatus _status); @@ -60,37 +65,15 @@ contract BaseTest is Test, Helpers { vm.etch(address(token), hex'069420'); module = new ForTest_ERC20ResolutionModule(oracle); - - mockDispute = - IOracle.Dispute({disputer: disputer, proposer: proposer, responseId: bytes32('69'), requestId: bytes32('69')}); - - mockResponse = IOracle.Response({proposer: _proposer, requestId: mockId, response: bytes('')}); } /** * @dev Helper function to cast votes. */ - function _populateVoters( - bytes32 _requestId, - bytes32 _disputeId, - uint256 _amountOfVoters, - uint256 _amountOfVotes, - IOracle.Request calldata _request - ) internal returns (uint256 _totalVotesCast) { - for (uint256 _i = 1; _i <= _amountOfVoters;) { + function _populateVoters(bytes32 _disputeId, uint256 _amountOfVoters, uint256 _amountOfVotes) internal { + for (uint256 _i = 1; _i <= _amountOfVoters; _i++) { vm.warp(120_000); - vm.startPrank(vm.addr(_i)); - vm.mockCall( - address(token), - abi.encodeCall(IERC20.transferFrom, (vm.addr(_i), address(module), _amountOfVotes)), - abi.encode() - ); - module.castVote(_request, mockDispute, _amountOfVotes); - vm.stopPrank(); - _totalVotesCast += _amountOfVotes; - unchecked { - ++_i; - } + module.forTest_castVote(_disputeId, vm.addr(_i), _amountOfVotes); } } } @@ -107,7 +90,6 @@ contract ERC20ResolutionModule_Unit_ModuleData is BaseTest { * @notice Test that the decodeRequestData function returns the correct values */ function test_decodeRequestData_returnsCorrectData( - bytes32 _requestId, address _token, uint256 _minVotesForQuorum, uint256 _votingTimeWindow @@ -129,56 +111,60 @@ contract ERC20ResolutionModule_Unit_StartResolution is BaseTest { /** * @notice Test that the `startResolution` is correctly called and the voting phase is started */ - function test_startResolution(bytes32 _disputeId, IOracle.Request calldata _request) public { + function test_revertIfNotOracle(bytes32 _disputeId) public { // Check: does revert if called by address != oracle? vm.expectRevert(IModule.Module_OnlyOracle.selector); - module.startResolution(_disputeId, _request, mockResponse, mockDispute); - - // Check: emits VotingPhaseStarted event? - vm.expectEmit(true, true, true, true); - emit VotingPhaseStarted(block.timestamp, _disputeId); + module.startResolution(_disputeId, mockRequest, mockResponse, mockDispute); + } + function test_setsStartTime(bytes32 _disputeId) public { vm.prank(address(oracle)); - module.startResolution(_disputeId, _request, mockResponse, mockDispute); - - (uint256 _startTime,) = module.escalations(_disputeId); + module.startResolution(_disputeId, mockRequest, mockResponse, mockDispute); // Check: `startTime` is set to block.timestamp? + (uint256 _startTime,) = module.escalations(_disputeId); assertEq(_startTime, block.timestamp); } + + function test_emitsEvent(bytes32 _disputeId) public { + // Check: emits VotingPhaseStarted event? + vm.expectEmit(true, true, true, true, address(module)); + emit VotingPhaseStarted(block.timestamp, _disputeId); + + vm.prank(address(oracle)); + module.startResolution(_disputeId, mockRequest, mockResponse, mockDispute); + } } contract ERC20ResolutionModule_Unit_CastVote is BaseTest { /** * @notice Test casting votes in valid voting time window. */ - function test_castVote( - bytes32 _requestId, - bytes32 _disputeId, - uint256 _amountOfVotes, - address _voter, - IOracle.Request calldata _request - ) public { - // Store mock dispute - mockDispute.requestId = _getId(_request); + function test_castVote(uint256 _amountOfVotes, address _voter) public { + uint256 _minVotesForQuorum = 1; - // Store mock escalation data with startTime 100_000 - module.forTest_setEscalation( - _disputeId, - IERC20ResolutionModule.Escalation({ - startTime: 100_000, - totalVotes: 0 // Initial amount of votes + mockRequest.resolutionModuleData = abi.encode( + IERC20ResolutionModule.RequestParameters({ + votingToken: token, + minVotesForQuorum: _minVotesForQuorum, + timeUntilDeadline: votingTimeWindow }) ); + mockDispute.requestId = _getId(mockRequest); + bytes32 _disputeId = _getId(mockDispute); - uint256 _minVotesForQuorum = 1; - uint256 _votingTimeWindow = 40_000; + // Store mock escalation data with startTime 100_000 + module.forTest_setStartTime(_disputeId, 100_000); // Mock and expect IERC20.transferFrom to be called _mockAndExpect( address(token), abi.encodeCall(IERC20.transferFrom, (_voter, address(module), _amountOfVotes)), abi.encode() ); + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated) + ); + // Warp to voting phase vm.warp(130_000); @@ -187,7 +173,7 @@ contract ERC20ResolutionModule_Unit_CastVote is BaseTest { emit VoteCast(_voter, _disputeId, _amountOfVotes); vm.prank(_voter); - module.castVote(_request, mockDispute, _amountOfVotes); + module.castVote(mockRequest, mockDispute, _amountOfVotes); (, uint256 _totalVotes) = module.escalations(_disputeId); // Check: totalVotes is updated? @@ -197,87 +183,75 @@ contract ERC20ResolutionModule_Unit_CastVote is BaseTest { assertEq(module.votes(_disputeId, _voter), _amountOfVotes); } - /** - * @notice Test that `castVote` reverts if there is no dispute with the given`_disputeId` - */ - function test_revertIfNonExistentDispute( - bytes32 _requestId, - bytes32 _disputeId, - uint256 _amountOfVotes, - IOracle.Request calldata _request - ) public { - // Default non-existent dispute - mockDispute.disputer = address(0); - mockDispute.responseId = bytes32(0); - mockDispute.proposer = address(0); - mockDispute.requestId = bytes32(0); - - // Check: reverts if called with `_disputeId` of a non-existent dispute? - vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_NonExistentDispute.selector); - module.castVote(_request, mockDispute, _amountOfVotes); - } - /** * @notice Test that `castVote` reverts if called with `_disputeId` of a non-escalated dispute. */ - function test_revertIfNotEscalated( - bytes32 _requestId, - bytes32 _disputeId, - uint256 _numberOfVotes, - IOracle.Request calldata _request - ) public { - // Mock the oracle response for looking up a dispute + function test_revertIfNotEscalated(uint256 _numberOfVotes) public { + bytes32 _requestId = _getId(mockRequest); mockDispute.requestId = _requestId; // Check: reverts if called with `_disputeId` of a non-escalated dispute? vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_DisputeNotEscalated.selector); - module.castVote(_request, mockDispute, _numberOfVotes); + module.castVote(mockRequest, mockDispute, _numberOfVotes); } /** * @notice Test that `castVote` reverts if called with `_disputeId` of an already resolved dispute. */ - function test_revertIfAlreadyResolved( - bytes32 _requestId, - bytes32 _disputeId, - uint256 _amountOfVotes, - IOracle.Request calldata _request - ) public { - // Mock dispute already resolved => DisputeStatus.Lost + function test_revertIfAlreadyResolved(uint256 _amountOfVotes, uint256 _votingTimeWindow) public { + mockRequest.resolutionModuleData = abi.encode( + IERC20ResolutionModule.RequestParameters({ + votingToken: token, + minVotesForQuorum: _amountOfVotes, + timeUntilDeadline: _votingTimeWindow + }) + ); + bytes32 _requestId = _getId(mockRequest); mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); + + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Won) + ); + + module.forTest_setStartTime(_disputeId, block.timestamp); // Check: reverts if dispute is already resolved? vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_AlreadyResolved.selector); - module.castVote(_request, mockDispute, _amountOfVotes); + module.castVote(mockRequest, mockDispute, _amountOfVotes); } /** * @notice Test that `castVote` reverts if called outside the voting time window. */ - function test_revertIfVotingPhaseOver( - bytes32 _requestId, - bytes32 _disputeId, - uint256 _numberOfVotes, - uint256 _timestamp, - IOracle.Request calldata _request - ) public { + function test_revertIfVotingPhaseOver(uint256 _numberOfVotes, uint256 _timestamp) public { vm.assume(_timestamp > 140_000); + uint256 _minVotesForQuorum = 1; - // Mock the oracle response for looking up a dispute + mockRequest.resolutionModuleData = abi.encode( + IERC20ResolutionModule.RequestParameters({ + votingToken: token, + minVotesForQuorum: _minVotesForQuorum, + timeUntilDeadline: votingTimeWindow + }) + ); + + bytes32 _requestId = _getId(mockRequest); mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); - module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated) + ); - // Store request data - uint256 _minVotesForQuorum = 1; - uint256 _votingTimeWindow = 40_000; + module.forTest_setStartTime(_disputeId, 100_000); // Jump to timestamp vm.warp(_timestamp); // Check: reverts if trying to cast vote after voting phase? vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_VotingPhaseOver.selector); - module.castVote(_request, mockDispute, _numberOfVotes); + module.castVote(mockRequest, mockDispute, _numberOfVotes); } } @@ -285,21 +259,27 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest { /** * @notice Test that a dispute is resolved, the tokens are transferred back to the voters and the dispute status updated. */ - function test_resolveDispute(IOracle.Request calldata _request, bytes32 _disputeId, uint16 _minVotesForQuorum) public { - // Store mock dispute and mock calls - bytes32 _requestId = _getId(_request); - mockDispute.requestId = _requestId; + function test_resolveDispute(uint16 _minVotesForQuorum) public { + mockRequest.resolutionModuleData = abi.encode( + IERC20ResolutionModule.RequestParameters({ + votingToken: token, + minVotesForQuorum: _minVotesForQuorum, + timeUntilDeadline: votingTimeWindow + }) + ); - // Store request data - uint256 _votingTimeWindow = 40_000; + bytes32 _requestId = _getId(mockRequest); + mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); // Store escalation data with `startTime` 100_000 and votes 0 - module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); + module.forTest_setStartTime(_disputeId, 100_000); uint256 _votersAmount = 5; // Make 5 addresses cast 100 votes each - uint256 _totalVotesCast = _populateVoters(_requestId, _disputeId, _votersAmount, 100, _request); + uint256 _totalVotesCast = 100 * _votersAmount; + _populateVoters(_disputeId, _votersAmount, 100); // Warp to resolving phase vm.warp(150_000); @@ -312,6 +292,10 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest { } } + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated) + ); + // If quorum reached, check for dispute status update and event emission IOracle.DisputeStatus _newStatus = _totalVotesCast >= _minVotesForQuorum ? IOracle.DisputeStatus.Won : IOracle.DisputeStatus.Lost; @@ -319,7 +303,7 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest { // Mock and expect IOracle.updateDisputeStatus to be called _mockAndExpect( address(oracle), - abi.encodeCall(IOracle.updateDisputeStatus, (_request, mockResponse, mockDispute, _newStatus)), + abi.encodeCall(IOracle.updateDisputeStatus, (mockRequest, mockResponse, mockDispute, _newStatus)), abi.encode() ); @@ -329,37 +313,36 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest { // Check: does revert if called by address != oracle? vm.expectRevert(IModule.Module_OnlyOracle.selector); - module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + module.resolveDispute(_disputeId, mockRequest, mockResponse, mockDispute); vm.prank(address(oracle)); - module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + module.resolveDispute(_disputeId, mockRequest, mockResponse, mockDispute); } /** * @notice Test that `resolveDispute` reverts if called during voting phase. */ - function test_revertIfOnGoingVotePhase( - IOracle.Request calldata _request, - bytes32 _disputeId, - uint256 _timestamp - ) public { + function test_revertIfOnGoingVotePhase(uint256 _timestamp) public { _timestamp = bound(_timestamp, 500_000, 999_999); - // Store mock dispute and mock calls - bytes32 _requestId = _getId(_request); - mockDispute.requestId = _requestId; + uint256 _minVotesForQuorum = 1; + uint256 _votingTimeWindow = 500_000; - module.forTest_setEscalation( - _disputeId, - IERC20ResolutionModule.Escalation({ - startTime: 500_000, - totalVotes: 0 // Initial amount of votes + mockRequest.resolutionModuleData = abi.encode( + IERC20ResolutionModule.RequestParameters({ + votingToken: token, + minVotesForQuorum: _minVotesForQuorum, + timeUntilDeadline: _votingTimeWindow }) ); + mockDispute.requestId = _getId(mockRequest); + bytes32 _disputeId = _getId(mockDispute); - // Store request data - uint256 _minVotesForQuorum = 1; - uint256 _votingTimeWindow = 500_000; + module.forTest_setStartTime(_disputeId, 500_000); + + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated) + ); // Jump to timestamp vm.warp(_timestamp); @@ -367,7 +350,7 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest { // Check: reverts if trying to resolve during voting phase? vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_OnGoingVotingPhase.selector); vm.prank(address(oracle)); - module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + module.resolveDispute(_disputeId, mockRequest, mockResponse, mockDispute); } } @@ -375,21 +358,16 @@ contract ERC20ResolutionModule_Unit_GetVoters is BaseTest { /** * @notice Test that `getVoters` returns an array of addresses of users that have voted. */ - function test_getVoters(IOracle.Request calldata _request, bytes32 _disputeId) public { - // Store mock dispute and mock calls - bytes32 _requestId = _getId(_request); + function test_getVoters(bytes32 _disputeId) public { + bytes32 _requestId = _getId(mockRequest); mockDispute.requestId = _requestId; - // Store request data - uint256 _votingTimeWindow = 40_000; - uint256 _minVotesForQuorum = 1; - // Store escalation data with `startTime` 100_000 and votes 0 - module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); + module.forTest_setStartTime(_disputeId, 100_000); uint256 _votersAmount = 3; // Make 3 addresses cast 100 votes each - _populateVoters(_requestId, _disputeId, _votersAmount, 100, _request); + _populateVoters(_disputeId, _votersAmount, 100); address[] memory _votersArray = module.getVoters(_disputeId);