From af489e0f63e7f2fa5c5214d8606a025c139e077a Mon Sep 17 00:00:00 2001 From: Gas <86567384+gas1cent@users.noreply.github.com> Date: Thu, 29 Jun 2023 19:07:26 +0400 Subject: [PATCH] feat: resolve disputes through oracle (#34) --- solidity/contracts/Oracle.sol | 15 ++ .../contracts/modules/ArbitratorModule.sol | 5 +- .../BondEscalationResolutionModule.sol | 2 +- .../modules/ERC20ResolutonModule.sol | 6 +- solidity/interfaces/IOracle.sol | 3 + solidity/test/unit/ArbitratorModule.t.sol | 17 +- .../unit/BondEscalationResolutionModule.t.sol | 9 + solidity/test/unit/Oracle.t.sol | 156 +++++++++++++----- 8 files changed, 152 insertions(+), 61 deletions(-) diff --git a/solidity/contracts/Oracle.sol b/solidity/contracts/Oracle.sol index b3e9f656..158d52a7 100644 --- a/solidity/contracts/Oracle.sol +++ b/solidity/contracts/Oracle.sol @@ -147,6 +147,21 @@ contract Oracle is IOracle { } } + function resolveDispute(bytes32 _disputeId) external { + Dispute memory _dispute = _disputes[_disputeId]; + + if (_dispute.createdAt == 0) revert Oracle_InvalidDisputeId(_disputeId); + // Revert if the dispute is not active nor escalated + unchecked { + if (uint256(_dispute.status) - 1 > 1) revert Oracle_CannotResolve(_disputeId); + } + + Request memory _request = _requests[_dispute.requestId]; + if (address(_request.resolutionModule) == address(0)) revert Oracle_NoResolutionModule(_disputeId); + + _request.resolutionModule.resolveDispute(_disputeId); + } + function updateDisputeStatus(bytes32 _disputeId, DisputeStatus _status) external { Dispute storage _dispute = _disputes[_disputeId]; Request memory _request = _requests[_dispute.requestId]; diff --git a/solidity/contracts/modules/ArbitratorModule.sol b/solidity/contracts/modules/ArbitratorModule.sol index 2137658a..057a072d 100644 --- a/solidity/contracts/modules/ArbitratorModule.sol +++ b/solidity/contracts/modules/ArbitratorModule.sol @@ -66,14 +66,11 @@ contract ArbitratorModule is Module, IArbitratorModule { } // Store the result of an Active dispute and flag it as Resolved - function resolveDispute(bytes32 _disputeId) external { + 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)); - - if (msg.sender != _arbitrator) revert ArbitratorModule_OnlyArbitrator(); - bool _valid = IArbitrator(_arbitrator).getAnswer(_disputeId); // Store the answer and the status as resolved diff --git a/solidity/contracts/modules/BondEscalationResolutionModule.sol b/solidity/contracts/modules/BondEscalationResolutionModule.sol index 5d027ab6..903cd183 100644 --- a/solidity/contracts/modules/BondEscalationResolutionModule.sol +++ b/solidity/contracts/modules/BondEscalationResolutionModule.sol @@ -289,7 +289,7 @@ contract BondEscalationResolutionModule is Module, IBondEscalationResolutionModu } } - function resolveDispute(bytes32 _disputeId) external { + function resolveDispute(bytes32 _disputeId) external onlyOracle { // Cache reused struct EscalationData storage _escalationData = escalationData[_disputeId]; diff --git a/solidity/contracts/modules/ERC20ResolutonModule.sol b/solidity/contracts/modules/ERC20ResolutonModule.sol index 97de69e9..534c45a3 100644 --- a/solidity/contracts/modules/ERC20ResolutonModule.sol +++ b/solidity/contracts/modules/ERC20ResolutonModule.sol @@ -104,11 +104,7 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule { emit VoteCast(msg.sender, _disputeId, _numberOfVotes); } - function resolveDispute(bytes32 _disputeId) external { - /* - TODO: check caller? - */ - + function resolveDispute(bytes32 _disputeId) external onlyOracle { // 0. Check that the disputeId actually exists IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId); if (_dispute.createdAt == 0) revert ERC20ResolutionModule_NonExistentDispute(); diff --git a/solidity/interfaces/IOracle.sol b/solidity/interfaces/IOracle.sol index df6c95a8..300eaf2c 100644 --- a/solidity/interfaces/IOracle.sol +++ b/solidity/interfaces/IOracle.sol @@ -16,6 +16,8 @@ interface IOracle { error Oracle_InvalidFinalizedResponse(bytes32 _responseId); error Oracle_InvalidDisputeId(bytes32 _disputeId); error Oracle_CannotEscalate(bytes32 _disputeId); + error Oracle_CannotResolve(bytes32 _disputeId); + error Oracle_NoResolutionModule(bytes32 _disputeId); struct Request { bytes requestModuleData; @@ -85,6 +87,7 @@ interface IOracle { function escalateDispute(bytes32 _disputeId) external; function getFinalizedResponse(bytes32 _requestId) external view returns (Response memory _response); function getResponseIds(bytes32 _requestId) external view returns (bytes32[] memory _ids); + function resolveDispute(bytes32 _disputeId) external; function updateDisputeStatus(bytes32 _disputeId, DisputeStatus _status) external; function getProposers(bytes32 _requestId) external view returns (address[] memory _proposers); function listRequests(uint256 _startFrom, uint256 _amount) external view returns (Request[] memory _list); diff --git a/solidity/test/unit/ArbitratorModule.t.sol b/solidity/test/unit/ArbitratorModule.t.sol index aac3ab37..ca8933ca 100644 --- a/solidity/test/unit/ArbitratorModule.t.sol +++ b/solidity/test/unit/ArbitratorModule.t.sol @@ -156,7 +156,7 @@ contract ArbitratorModule_UnitTest is Test { ); // Test: resolve the dispute - vm.prank(address(arbitrator)); + vm.prank(address(oracle)); arbitratorModule.resolveDispute(_disputeId); // Check: status is now Resolved? @@ -195,7 +195,7 @@ contract ArbitratorModule_UnitTest is Test { vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidDisputeId.selector)); // Test: try calling resolve - vm.prank(address(arbitrator)); + vm.prank(address(oracle)); arbitratorModule.resolveDispute(_disputeId); } } @@ -204,21 +204,14 @@ contract ArbitratorModule_UnitTest is Test { * @notice Test that the resolve function reverts if the caller isn't the arbitrator */ function test_resolveDisputeWrongSenderReverts(bytes32 _disputeId, bytes32 _requestId, address _caller) public { - vm.assume(_caller != address(arbitrator)); - - // Mock and expect the dummy dispute - mockDispute.requestId = _requestId; - mockDispute.status = IOracle.DisputeStatus.Escalated; - - vm.mockCall(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(mockDispute)); - vm.expectCall(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId))); + vm.assume(_caller != address(oracle)); // Store the mock dispute bytes memory _requestData = abi.encode(address(arbitrator)); arbitratorModule.forTest_setRequestData(_requestId, _requestData); - // Check: revert - vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_OnlyArbitrator.selector)); + // Check: revert? + vm.expectRevert(IModule.Module_OnlyOracle.selector); // Test: resolve the dispute vm.prank(_caller); diff --git a/solidity/test/unit/BondEscalationResolutionModule.t.sol b/solidity/test/unit/BondEscalationResolutionModule.t.sol index c1354977..f6998d31 100644 --- a/solidity/test/unit/BondEscalationResolutionModule.t.sol +++ b/solidity/test/unit/BondEscalationResolutionModule.t.sol @@ -356,12 +356,15 @@ contract BondEscalationResolutionModule_UnitTest is Test { _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_AlreadyResolved.selector); + vm.prank(address(oracle)); module.resolveDispute(_disputeId); // Revert if dispute not escalated _resolution = IBondEscalationResolutionModule.Resolution.Unresolved; _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_NotEscalated.selector); + vm.prank(address(oracle)); module.resolveDispute(_disputeId); // Revert if we have not yet reached the deadline and the timer has not passed @@ -381,7 +384,9 @@ contract BondEscalationResolutionModule_UnitTest is Test { _startTime = uint128(block.timestamp); _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_PledgingPhaseNotOver.selector); + vm.prank(address(oracle)); module.resolveDispute(_disputeId); } @@ -403,6 +408,7 @@ contract BondEscalationResolutionModule_UnitTest is Test { // START OF TEST THRESHOLD NOT REACHED uint256 _pledgeThreshold = 1000; _setRequestData(_requestId, percentageDiff, _pledgeThreshold, timeUntilDeadline, timeToBreakInequality); + vm.prank(address(oracle)); module.resolveDispute(_disputeId); (IBondEscalationResolutionModule.Resolution _trueResStatus,,,) = module.escalationData(_disputeId); @@ -429,6 +435,7 @@ contract BondEscalationResolutionModule_UnitTest is Test { // START OF TIED PLEDGES _setRequestData(_requestId, percentageDiff, pledgeThreshold, timeUntilDeadline, timeToBreakInequality); + vm.prank(address(oracle)); module.resolveDispute(_disputeId); (IBondEscalationResolutionModule.Resolution _trueResStatus,,,) = module.escalationData(_disputeId); @@ -463,6 +470,7 @@ contract BondEscalationResolutionModule_UnitTest is Test { ); vm.expectCall(address(oracle), abi.encodeCall(IOracle.updateDisputeStatus, (_disputeId, IOracle.DisputeStatus.Won))); + vm.prank(address(oracle)); module.resolveDispute(_disputeId); (IBondEscalationResolutionModule.Resolution _trueResStatus,,,) = module.escalationData(_disputeId); @@ -499,6 +507,7 @@ contract BondEscalationResolutionModule_UnitTest is Test { address(oracle), abi.encodeCall(IOracle.updateDisputeStatus, (_disputeId, IOracle.DisputeStatus.Lost)) ); + vm.prank(address(oracle)); module.resolveDispute(_disputeId); (IBondEscalationResolutionModule.Resolution _trueResStatus,,,) = module.escalationData(_disputeId); diff --git a/solidity/test/unit/Oracle.t.sol b/solidity/test/unit/Oracle.t.sol index 798c3685..65d683d5 100644 --- a/solidity/test/unit/Oracle.t.sol +++ b/solidity/test/unit/Oracle.t.sol @@ -23,15 +23,19 @@ import {IModule} from '../../interfaces/IModule.sol'; contract ForTest_Oracle is Oracle { constructor() Oracle() {} - function forTest_setResponse(IOracle.Response calldata _response) external returns (bytes32 _responseId) { + function forTest_setResponse(Response calldata _response) external returns (bytes32 _responseId) { _responseId = keccak256(abi.encodePacked(msg.sender, address(this), _response.requestId)); _responses[_responseId] = _response; _responseIds[_response.requestId].push(_responseId); } - function forTest_setDisputes(bytes32 _disputeId, IOracle.Dispute calldata _dispute) external { + function forTest_setDispute(bytes32 _disputeId, Dispute calldata _dispute) external { _disputes[_disputeId] = _dispute; } + + function forTest_setRequest(bytes32 _requestId, Request calldata _request) external { + _requests[_requestId] = _request; + } } /** @@ -47,15 +51,17 @@ contract Oracle_UnitTest is Test { address public sender = makeAddr('sender'); IRequestModule public requestModule = IRequestModule(makeAddr('requestModule')); - IResponseModule public responseModule = IResponseModule(makeAddr('responseModule')); - IDisputeModule public disputeModule = IDisputeModule(makeAddr('disputeModule')); - IResolutionModule public resolutionModule = IResolutionModule(makeAddr('resolutionModule')); - IFinalityModule public finalityModule = IFinalityModule(makeAddr('finalityModule')); + // 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'); + /** * @notice Deploy the target and mock oracle+modules */ @@ -66,6 +72,15 @@ contract Oracle_UnitTest is Test { vm.etch(address(disputeModule), hex'69'); vm.etch(address(resolutionModule), hex'69'); vm.etch(address(finalityModule), hex'69'); + + mockDispute = IOracle.Dispute({ + createdAt: block.timestamp, + disputer: sender, + proposer: sender, + responseId: mockId, + requestId: mockId, + status: IOracle.DisputeStatus.Active + }); } /** @@ -208,7 +223,7 @@ contract Oracle_UnitTest is Test { // Test: fetching the requests IOracle.Request[] memory _requests = oracle.listRequests(0, _howMany); - // Check: enought request returned? + // Check: enough request returned? assertEq(_requests.length, _howMany); // Check: correct requests returned (dummy are incremented)? @@ -337,21 +352,11 @@ contract Oracle_UnitTest is Test { // Compute the dispute ID bytes32 _disputeId = keccak256(abi.encodePacked(sender, _requestId, _responseId)); - // Create mock dispute - IOracle.Dispute memory _dispute = IOracle.Dispute({ - createdAt: block.timestamp, - disputer: sender, - proposer: _proposer, - responseId: _responseId, - requestId: _requestId, - status: IOracle.DisputeStatus.Active - }); - // Mock&expect the disputeModule disputeResponse call vm.mockCall( address(disputeModule), abi.encodeCall(IDisputeModule.disputeResponse, (_requestId, _responseId, sender, _proposer)), - abi.encode(_dispute) + abi.encode(mockDispute) ); vm.expectCall( address(disputeModule), @@ -368,12 +373,12 @@ contract Oracle_UnitTest is Test { IOracle.Dispute memory _storedDispute = oracle.getDispute(_disputeId); // Check: correct dispute stored? - assertEq(_storedDispute.createdAt, _dispute.createdAt); - assertEq(_storedDispute.disputer, _dispute.disputer); - assertEq(_storedDispute.proposer, _dispute.proposer); - assertEq(_storedDispute.responseId, _dispute.responseId); - assertEq(_storedDispute.requestId, _dispute.requestId); - assertEq(uint256(_storedDispute.status), uint256(_dispute.status)); + assertEq(_storedDispute.createdAt, mockDispute.createdAt); + assertEq(_storedDispute.disputer, mockDispute.disputer); + assertEq(_storedDispute.proposer, mockDispute.proposer); + assertEq(_storedDispute.responseId, mockDispute.responseId); + assertEq(_storedDispute.requestId, mockDispute.requestId); + assertEq(uint256(_storedDispute.status), uint256(mockDispute.status)); } /** @@ -409,33 +414,27 @@ contract Oracle_UnitTest is Test { bytes32 _disputeId = bytes32('69'); // Try every initial status - for (uint256 _previousStatus; _previousStatus < 4; _previousStatus++) { + for (uint256 _previousStatus; _previousStatus < uint256(type(IOracle.DisputeStatus).max); _previousStatus++) { // Try every new status - for (uint256 _newStatus; _newStatus < 4; _newStatus++) { - // Create a new dummy dispute - IOracle.Dispute memory _dispute = IOracle.Dispute({ - createdAt: block.timestamp, - disputer: sender, - proposer: sender, - responseId: bytes32('69'), - requestId: _requestId, - status: IOracle.DisputeStatus(_previousStatus) - }); + for (uint256 _newStatus; _newStatus < uint256(type(IOracle.DisputeStatus).max); _newStatus++) { + // Set the dispute status + mockDispute.status = IOracle.DisputeStatus(_previousStatus); + mockDispute.requestId = _requestId; // Set this new dispute, overwriting the one from the previous iteration - oracle.forTest_setDisputes(_disputeId, _dispute); + oracle.forTest_setDispute(_disputeId, mockDispute); // The mocked call is done with the new status - _dispute.status = IOracle.DisputeStatus(_newStatus); + mockDispute.status = IOracle.DisputeStatus(_newStatus); // Mock&expect the disputeModule updateDisputeStatus call vm.mockCall( address(disputeModule), - abi.encodeCall(IDisputeModule.updateDisputeStatus, (_disputeId, _dispute)), + abi.encodeCall(IDisputeModule.updateDisputeStatus, (_disputeId, mockDispute)), abi.encode() ); vm.expectCall( - address(disputeModule), abi.encodeCall(IDisputeModule.updateDisputeStatus, (_disputeId, _dispute)) + address(disputeModule), abi.encodeCall(IDisputeModule.updateDisputeStatus, (_disputeId, mockDispute)) ); // Test: change the status @@ -449,6 +448,85 @@ contract Oracle_UnitTest is Test { } } + /** + * @notice resolveDispute is expected to call resolution module + */ + function test_resolveDispute() public { + // Create mock request and store it + bytes32 _requestId = _storeDummyRequests(1)[0]; + + // Create a dummy dispute + bytes32 _disputeId = bytes32('69'); + mockDispute.requestId = _requestId; + oracle.forTest_setDispute(_disputeId, mockDispute); + + // Mock and expect the resolution module call + vm.mockCall(address(resolutionModule), abi.encodeCall(IResolutionModule.resolveDispute, (_disputeId)), abi.encode()); + vm.expectCall(address(resolutionModule), abi.encodeCall(IResolutionModule.resolveDispute, (_disputeId))); + + // Test: resolve the dispute + oracle.resolveDispute(_disputeId); + } + + /** + * @notice Test the revert when the function is called with an non-existent dispute id + */ + function test_resolveDisputeRevertsIfInvalidDispute(bytes32 _disputeId) public { + // Check: revert? + vm.expectRevert(abi.encodeWithSelector(IOracle.Oracle_InvalidDisputeId.selector, _disputeId)); + + // Test: try to resolve the dispute + oracle.resolveDispute(_disputeId); + } + + /** + * @notice Test the revert when the function is called but no resolution module was configured + */ + function test_resolveDisputeRevertsIfWrongDisputeStatus() public { + // Create a dummy dispute + bytes32 _disputeId = bytes32('69'); + + for (uint256 _status; _status < uint256(type(IOracle.DisputeStatus).max); _status++) { + if ( + IOracle.DisputeStatus(_status) == IOracle.DisputeStatus.Active + || IOracle.DisputeStatus(_status) == IOracle.DisputeStatus.Escalated + ) continue; + // Set the dispute status + mockDispute.status = IOracle.DisputeStatus(_status); + + // Set this new dispute, overwriting the one from the previous iteration + oracle.forTest_setDispute(_disputeId, mockDispute); + + // Check: revert? + vm.expectRevert(abi.encodeWithSelector(IOracle.Oracle_CannotResolve.selector, _disputeId)); + + // Test: try to resolve the dispute + oracle.resolveDispute(_disputeId); + } + } + + /** + * @notice Test the revert when the function is called with a non-active and non-escalated dispute + */ + function test_resolveDisputeRevertsIfNoResolutionModule() public { + // Create a dummy dispute + bytes32 _disputeId = bytes32('69'); + oracle.forTest_setDispute(_disputeId, mockDispute); + + // Change the request of this dispute so that it does not have a resolution module + bytes32 _requestId = _storeDummyRequests(1)[0]; + IOracle.Request memory _request = oracle.getRequest(_requestId); + _request.resolutionModule = IResolutionModule(address(0)); + oracle.forTest_setRequest(_requestId, _request); + mockDispute.requestId = _requestId; + + // Check: revert? + vm.expectRevert(abi.encodeWithSelector(IOracle.Oracle_NoResolutionModule.selector, _disputeId)); + + // Test: try to resolve the dispute + oracle.resolveDispute(_disputeId); + } + /** * @notice update dispute status revert if sender not resolution module */