Skip to content

Commit

Permalink
feat: escalate disputes through oracle (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
gas1cent authored Jun 23, 2023
1 parent e025c12 commit 0155321
Show file tree
Hide file tree
Showing 13 changed files with 91 additions and 115 deletions.
21 changes: 21 additions & 0 deletions solidity/contracts/Oracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,27 @@ contract Oracle is IOracle {
disputeOf[_responseId] = _disputeId;
}

function escalateDispute(bytes32 _disputeId) external {
Dispute memory _dispute = _disputes[_disputeId];

if (_dispute.createdAt == 0) revert Oracle_InvalidDisputeId(_disputeId);
if (_dispute.status != DisputeStatus.Active) revert Oracle_CannotEscalate(_disputeId);

// Change the dispute status
_dispute.status = DisputeStatus.Escalated;
_disputes[_disputeId] = _dispute;

Request memory _request = _requests[_dispute.requestId];

// Notify the dispute module about the escalation
_request.disputeModule.disputeEscalated(_disputeId);

if (address(_request.resolutionModule) != address(0)) {
// Initiate the resolution
_request.resolutionModule.startResolution(_disputeId);
}
}

function updateDisputeStatus(bytes32 _disputeId, DisputeStatus _status) external {
Dispute storage _dispute = _disputes[_disputeId];
Request memory _request = _requests[_dispute.requestId];
Expand Down
12 changes: 3 additions & 9 deletions solidity/contracts/modules/ArbitratorModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,24 @@ contract ArbitratorModule is Module, IArbitratorModule {
function getStatus(bytes32 _disputeId) external view returns (ArbitrationStatus _disputeStatus) {
uint256 _currentDisputeData = _disputeData[_disputeId];

_disputeStatus = IArbitratorModule.ArbitrationStatus(_currentDisputeData & 3);
_disputeStatus = ArbitrationStatus(_currentDisputeData & 3);
}

// Gets the dispute from the pre-dispute module and opens it for resolution

// call the arbitrator with the dispute to arbitrate (it might or might not answer atomically -> eg queue a snapshot
// vote vs a chainlink call) -> atomically should happen during a callback to storeAnswer
//
// arbitrator can either be an EOA (or contract) which sends dispute resolution to this contract (ie offchain vote later sent)
// or a contract which implements IArbitrator
// or a contract which resolve as soon as being called (ie chainlink wrapper), in a callback
function escalateDispute(bytes32 _disputeId) external {
function startResolution(bytes32 _disputeId) external onlyOracle {
IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId);
if (_dispute.status != IOracle.DisputeStatus.Active) revert ArbitratorModule_InvalidDisputeId();
if (!ORACLE.validModule(_dispute.requestId, msg.sender)) revert Module_InvalidCaller();

address _arbitrator = abi.decode(requestData[_dispute.requestId], (address));

// Prevent dead-lock if incorrect address
if (_arbitrator == address(0)) revert ArbitratorModule_InvalidArbitrator();

// Change the dispute status
ORACLE.updateDisputeStatus(_disputeId, IOracle.DisputeStatus.Escalated);

// Mark the dispute as Active for the arbitrator
_disputeData[_disputeId] = 1;

Expand All @@ -78,7 +72,7 @@ contract ArbitratorModule is Module, IArbitratorModule {
// Store the result of an Active dispute and flag it as Resolved
function resolveDispute(bytes32 _disputeId) external {
IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId);
if (_dispute.status != IOracle.DisputeStatus.Active) revert ArbitratorModule_InvalidDisputeId();
if (_dispute.status != IOracle.DisputeStatus.Escalated) revert ArbitratorModule_InvalidDisputeId();

address _arbitrator = abi.decode(requestData[_dispute.requestId], (address));

Expand Down
5 changes: 2 additions & 3 deletions solidity/contracts/modules/BondEscalationModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ contract BondEscalationModule is Module, IBondEscalationModule {
}

/**
* @notice Escalates a dispute.
* @notice Verifies that the escalated dispute has reached a tie and updates its escalation status.
*
* @dev If the bond escalation window is over and the dispute is the first dispute of the request,
* It will check whether the dispute has been previously escalated, and if it hasn't, it will
Expand All @@ -34,7 +34,7 @@ contract BondEscalationModule is Module, IBondEscalationModule {
*
* @param _disputeId The ID of the dispute to escalate.
*/
function escalateDispute(bytes32 _disputeId) external {
function disputeEscalated(bytes32 _disputeId) external onlyOracle {
IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId);

if (_dispute.requestId == bytes32(0)) revert BondEscalationModule_DisputeDoesNotExist();
Expand Down Expand Up @@ -66,7 +66,6 @@ contract BondEscalationModule is Module, IBondEscalationModule {

bondEscalationStatus[_dispute.requestId] = BondEscalationStatus.Escalated;
}
// TODO: Start the real dispute process, involving the arbitrator
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,8 @@ contract BondEscalationResolutionModule is Module, IBondEscalationResolutionModu
abi.decode(_data, (IBondEscalationAccounting, IERC20, uint256, uint256, uint256, uint256));
}

function escalateDispute(bytes32 _disputeId) external {
function startResolution(bytes32 _disputeId) external onlyOracle {
bytes32 _requestId = ORACLE.getDispute(_disputeId).requestId;
IDisputeModule _disputeModule = ORACLE.getRequest(_requestId).disputeModule;
if (msg.sender != address(_disputeModule)) revert BondEscalationResolutionModule_OnlyDisputeModule();
escalationData[_disputeId].startTime = uint128(block.timestamp);
emit DisputeEscalated(_disputeId, _requestId);
}
Expand Down
5 changes: 1 addition & 4 deletions solidity/contracts/modules/BondedDisputeModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ contract BondedDisputeModule is Module, IBondedDisputeModule {
(_accountingExtension, _bondToken, _bondSize) = _decodeRequestData(requestData[_requestId]);
}

// TODO: adding an Escalated status to the disputeStatus enum could work to avoid escalating two disputes twice
function escalateDispute(bytes32 _disputeId) external {
// TODO: Start the real dispute process, involving the arbitrator
}
function disputeEscalated(bytes32 _disputeId) external onlyOracle {}

function disputeResponse(
bytes32 _requestId,
Expand Down
4 changes: 1 addition & 3 deletions solidity/contracts/modules/ERC20ResolutonModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,8 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule {
abi.decode(_data, (IAccountingExtension, IERC20, uint256, uint256, uint256));
}

function escalateDispute(bytes32 _disputeId) external {
function startResolution(bytes32 _disputeId) external onlyOracle {
bytes32 _requestId = ORACLE.getDispute(_disputeId).requestId;
IDisputeModule _disputeModule = ORACLE.getRequest(_requestId).disputeModule;
if (msg.sender != address(_disputeModule)) revert ERC20ResolutionModule_OnlyDisputeModule();
(IAccountingExtension _accounting, IERC20 _token, uint256 _disputerBondSize,,) = decodeRequestData(_requestId);

escalationData[_disputeId].startTime = uint128(block.timestamp);
Expand Down
6 changes: 5 additions & 1 deletion solidity/interfaces/IOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ interface IOracle {
error Oracle_ResponseAlreadyDisputed(bytes32 _responseId);
error Oracle_AlreadyFinalized(bytes32 _requestId);
error Oracle_InvalidFinalizedResponse(bytes32 _responseId);
error Oracle_InvalidDisputeId(bytes32 _disputeId);
error Oracle_CannotEscalate(bytes32 _disputeId);

struct Request {
bytes requestModuleData;
Expand Down Expand Up @@ -57,7 +59,8 @@ interface IOracle {
Active,
Escalated,
Won,
Lost
Lost,
NoResolution
}

/**
Expand All @@ -82,6 +85,7 @@ interface IOracle {
function disputeOf(bytes32 _requestId) external view returns (bytes32 _disputeId);
function proposeResponse(bytes32 _requestId, bytes calldata _responseData) external returns (bytes32 _responseId);
function disputeResponse(bytes32 _requestId, bytes32 _responseId) external returns (bytes32 _disputeId);
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 updateDisputeStatus(bytes32 _disputeId, DisputeStatus _status) external;
Expand Down
1 change: 1 addition & 0 deletions solidity/interfaces/modules/IDisputeModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ interface IDisputeModule is IModule {
) external returns (IOracle.Dispute memory _dispute);

function updateDisputeStatus(bytes32 _disputeId, IOracle.Dispute memory _dispute) external;
function disputeEscalated(bytes32 _disputeId) external;
}
2 changes: 1 addition & 1 deletion solidity/interfaces/modules/IResolutionModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import {IOracle} from '../IOracle.sol';

interface IResolutionModule is IModule {
function resolveDispute(bytes32 _disputeId) external;
function escalateDispute(bytes32 _disputeId) external;
function startResolution(bytes32 _disputeId) external;
}
85 changes: 24 additions & 61 deletions solidity/test/unit/ArbitratorModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ contract ArbitratorModule_UnitTest is Test {

// 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)));

Expand All @@ -160,7 +162,7 @@ contract ArbitratorModule_UnitTest is Test {
arbitratorModule.resolveDispute(_disputeId);

// Check: status is now Resolved?
assertEq(uint256(arbitratorModule.getStatus(_disputeId)), 2);
assertEq(uint256(arbitratorModule.getStatus(_disputeId)), uint256(IArbitratorModule.ArbitrationStatus.Resolved));

// Check: dispute has correct isValid flag?
assertEq(arbitratorModule.isValid(_disputeId), _valid);
Expand All @@ -170,9 +172,13 @@ contract ArbitratorModule_UnitTest is Test {
* @notice resolve dispute reverts if the dispute status isn't Active
*/
function test_resolveDisputeInvalidDisputeReverts(bytes32 _disputeId) public {
// Store the requestData
bytes memory _requestData = abi.encode(address(arbitrator));
arbitratorModule.forTest_setRequestData(mockId, _requestData);

// Test the 3 different invalid status (None, Won, Lost)
for (uint256 _status; _status < 4; _status++) {
if (_status == 1) continue; // skip the valid status (Active)
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,
Expand All @@ -191,6 +197,7 @@ contract ArbitratorModule_UnitTest is Test {
vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidDisputeId.selector));

// Test: try calling resolve
vm.prank(address(arbitrator));
arbitratorModule.resolveDispute(_disputeId);
}
}
Expand All @@ -203,6 +210,8 @@ contract ArbitratorModule_UnitTest is Test {

// 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)));

Expand All @@ -222,91 +231,45 @@ contract ArbitratorModule_UnitTest is Test {
/**
* @notice Test that the escalate function works as expected
*/
function test_escalate(bytes32 _disputeId, bytes32 _requestId) public {
function test_startResolution(bytes32 _disputeId, bytes32 _requestId) public {
// Mock and expect the dummy dispute
mockDispute.requestId = _requestId;
vm.mockCall(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(mockDispute));
vm.expectCall(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)));

// Mock and expect the validModule call
vm.mockCall(address(oracle), abi.encodeCall(oracle.validModule, (_requestId, dude)), abi.encode(true));
vm.expectCall(address(oracle), abi.encodeCall(oracle.validModule, (_requestId, dude)));

// Store the requestData
bytes memory _requestData = abi.encode(address(arbitrator));
arbitratorModule.forTest_setRequestData(_requestId, _requestData);

// Mock and expect the status update call
vm.mockCall(
address(oracle),
abi.encodeCall(oracle.updateDisputeStatus, (_disputeId, IOracle.DisputeStatus.Escalated)),
abi.encode()
);
vm.expectCall(
address(oracle), abi.encodeCall(oracle.updateDisputeStatus, (_disputeId, IOracle.DisputeStatus.Escalated))
);

// Mock and expect the callback to the arbitrator
vm.mockCall(address(arbitrator), abi.encodeCall(arbitrator.resolve, (_disputeId)), abi.encode(bytes('')));
vm.expectCall(address(arbitrator), abi.encodeCall(arbitrator.resolve, (_disputeId)));

// Test: escalate the dispute
vm.prank(dude);
arbitratorModule.escalateDispute(_disputeId);
vm.prank(address(oracle));
arbitratorModule.startResolution(_disputeId);

// Check: status is now Active?
assertEq(uint256(arbitratorModule.getStatus(_disputeId)), 1);
// Check: status is now Escalated?
assertEq(uint256(arbitratorModule.getStatus(_disputeId)), uint256(IArbitratorModule.ArbitrationStatus.Active));
}

// Revert is dispute not active
function test_escalateRevertIfInactiveDispute(bytes32 _disputeId, bytes32 _requestId) public {
for (uint256 i; i < 4; i++) {
if (i == 1) continue; // skip the valid status (Active)

// Mock and expect the dummy dispute
mockDispute.requestId = _requestId;
mockDispute.status = IOracle.DisputeStatus(i);
vm.mockCall(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(mockDispute));
vm.expectCall(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)));

// Check: revert?
vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidDisputeId.selector));

// Test: escalate the dispute
arbitratorModule.escalateDispute(_disputeId);
}
}

// Revert if caller isn't valid module
function test_escalateRevertCallerInvalidModule(bytes32 _disputeId, bytes32 _requestId) public {
// Mock and expect the dummy dispute
mockDispute.requestId = _requestId;
vm.mockCall(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(mockDispute));
vm.expectCall(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)));

// Mock and expect the validModule call
vm.mockCall(address(oracle), abi.encodeCall(oracle.validModule, (_requestId, dude)), abi.encode(false));
vm.expectCall(address(oracle), abi.encodeCall(oracle.validModule, (_requestId, dude)));

// Revert if caller isn't the oracle
function test_startResolutionRevertInvalidCaller(bytes32 _disputeId) public {
// Check: revert?
vm.expectRevert(abi.encodeWithSelector(IModule.Module_InvalidCaller.selector));
vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector));

// Test: escalate the dispute
vm.prank(dude);
arbitratorModule.escalateDispute(_disputeId);
arbitratorModule.startResolution(_disputeId);
}

// Revert if wrong arbitrator
function test_escalateRevertIfEmptyArbitror(bytes32 _disputeId, bytes32 _requestId) public {
function test_startResolutionRevertIfEmptyArbitrator(bytes32 _disputeId, bytes32 _requestId) public {
// Mock and expect the dummy dispute
mockDispute.requestId = _requestId;
vm.mockCall(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)), abi.encode(mockDispute));
vm.expectCall(address(oracle), abi.encodeCall(oracle.getDispute, (_disputeId)));

// Mock and expect the validModule call
vm.mockCall(address(oracle), abi.encodeCall(oracle.validModule, (_requestId, dude)), abi.encode(true));
vm.expectCall(address(oracle), abi.encodeCall(oracle.validModule, (_requestId, dude)));

// Store the requestData
bytes memory _requestData = abi.encode(address(0));
arbitratorModule.forTest_setRequestData(_requestId, _requestData);
Expand All @@ -315,8 +278,8 @@ contract ArbitratorModule_UnitTest is Test {
vm.expectRevert(abi.encodeWithSelector(IArbitratorModule.ArbitratorModule_InvalidArbitrator.selector));

// Test: escalate the dispute
vm.prank(dude);
arbitratorModule.escalateDispute(_disputeId);
vm.prank(address(oracle));
arbitratorModule.startResolution(_disputeId);
}

/**
Expand Down
Loading

0 comments on commit 0155321

Please sign in to comment.