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..1da2a6bd 100644 --- a/solidity/contracts/modules/dispute/BondedDisputeModule.sol +++ b/solidity/contracts/modules/dispute/BondedDisputeModule.sol @@ -1,115 +1,106 @@ -// // 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, + _disputeId: _getId(_dispute), + _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..cb93edb1 100644 --- a/solidity/test/unit/modules/dispute/BondedDisputeModule.t.sol +++ b/solidity/test/unit/modules/dispute/BondedDisputeModule.t.sol @@ -1,407 +1,256 @@ -// // 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; + + 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); + } +} + +contract BondedResponseModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData_returnsCorrectData( + address _accountingExtension, + address _token, + uint256 _bondSize + ) public { + // Mock data + bytes memory _requestData = abi.encode(_accountingExtension, _token, _bondSize); + + // 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 Dispute lost by disputer + */ + 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 + _mockAndExpect( + address(accountingExtension), + 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, (mockResponse.proposer, _requestId, _token, _bondSize)), + abi.encode() + ); + + vm.prank(address(oracle)); + bondedDisputeModule.onDisputeStatusChange(_getId(mockDispute), mockRequest, mockResponse, mockDispute); + } + + /** + * @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 + _mockAndExpect( + address(accountingExtension), + 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, (mockDispute.disputer, _requestId, _token, _bondSize)), + abi.encode() + ); + + vm.prank(address(oracle)); + bondedDisputeModule.onDisputeStatusChange(_getId(mockDispute), mockRequest, mockResponse, mockDispute); + } + + /** + * @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, (mockResponse.proposer, _requestId, _token, _bondSize)), + abi.encode() + ); + + // Mock and expect the call to release, for the disputer + _mockAndExpect( + address(accountingExtension), + abi.encodeCall(accountingExtension.release, (mockDispute.disputer, _requestId, _token, _bondSize)), + abi.encode() + ); + + vm.prank(address(oracle)); + bondedDisputeModule.onDisputeStatusChange(_getId(mockDispute), mockRequest, mockResponse, mockDispute); + } + + function test_statusWithNoChange(uint256 _bondSize, IERC20 _token) public { + // Mock request data + 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); + } + } + + /** + * @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(_getId(mockDispute), mockRequest, mockResponse, mockDispute); + } +} + +contract BondedResponseModule_Unit_DisputeResponse is BaseTest { + /** + * @notice Test if dispute response returns the correct status + */ + function test_createBond(uint256 _bondSize, IERC20 _token) public { + // Mock request data + 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)', mockDispute.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/yarn.lock b/yarn.lock index b61d241d..e3e8e0d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"