diff --git a/contracts/domain/BosonTypes.sol b/contracts/domain/BosonTypes.sol index 975af496f..7cc2dd6e2 100644 --- a/contracts/domain/BosonTypes.sol +++ b/contracts/domain/BosonTypes.sol @@ -193,6 +193,9 @@ contract BosonTypes { struct Dispute { uint256 exchangeId; uint256 buyerPercent; + uint256 sellerPercent; + bool buyerPercentSet; + bool sellerPercentSet; DisputeState state; } diff --git a/contracts/interfaces/escalation/IEscalatable.sol b/contracts/interfaces/escalation/IEscalatable.sol new file mode 100644 index 000000000..7e542b870 --- /dev/null +++ b/contracts/interfaces/escalation/IEscalatable.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8; + +interface IEscalatable { + function escalateDispute( + uint256 _exchangeId, + uint256 _buyerPercent, + uint256 _sellerPercent + ) external; + + function escalationCost() external returns (uint256 _cost); +} diff --git a/contracts/interfaces/escalation/IEscalationResolver.sol b/contracts/interfaces/escalation/IEscalationResolver.sol new file mode 100644 index 000000000..0e37c9dd5 --- /dev/null +++ b/contracts/interfaces/escalation/IEscalationResolver.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8; + +interface IEscalationResolver { + function decideDispute(uint256 _exchangeId, uint256 _buyerPercent) external; + + function refuseEscalatedDispute(uint256 _exchangeId) external; +} diff --git a/contracts/interfaces/handlers/IBosonDisputeHandler.sol b/contracts/interfaces/handlers/IBosonDisputeHandler.sol index 319b24548..5fcdf6c25 100644 --- a/contracts/interfaces/handlers/IBosonDisputeHandler.sol +++ b/contracts/interfaces/handlers/IBosonDisputeHandler.sol @@ -115,18 +115,9 @@ interface IBosonDisputeHandler is IBosonDisputeEvents, IBosonFundsLibEvents { * - Dispute was escalated and escalation period has elapsed * * @param _exchangeId - the id of the associated exchange - * @param _buyerPercent - percentage of the pot that goes to the buyer - * @param _sigR - r part of the signer's signature - * @param _sigS - s part of the signer's signature - * @param _sigV - v part of the signer's signature + * @param _percent - percentage of the pot that goes to the buyer */ - function resolveDispute( - uint256 _exchangeId, - uint256 _buyerPercent, - bytes32 _sigR, - bytes32 _sigS, - uint8 _sigV - ) external; + function resolveDispute(uint256 _exchangeId, uint256 _percent) external; /** * @notice Puts the dispute into the Escalated state. @@ -211,14 +202,12 @@ interface IBosonDisputeHandler is IBosonDisputeEvents, IBosonFundsLibEvents { * @return dispute - the dispute details. See {BosonTypes.Dispute} * @return disputeDates - the dispute dates details {BosonTypes.DisputeDates} */ - function getDispute(uint256 _exchangeId) + function getDispute( + uint256 _exchangeId + ) external view - returns ( - bool exists, - BosonTypes.Dispute memory dispute, - BosonTypes.DisputeDates memory disputeDates - ); + returns (bool exists, BosonTypes.Dispute memory dispute, BosonTypes.DisputeDates memory disputeDates); /** * @notice Gets the state of a given dispute. diff --git a/contracts/kleros/BosonKlerosConnector.sol b/contracts/kleros/BosonKlerosConnector.sol new file mode 100644 index 000000000..b6b8bf936 --- /dev/null +++ b/contracts/kleros/BosonKlerosConnector.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8; +// THIS CONTRACT IS IMPLEMENTED FOR INOFRMATION PURPOSES AND SHOULD NOT BE USED IN PRODUCTION + +import { IArbitrator } from "./IArbitrator.sol"; +import { IArbitrable } from "./IArbitrable.sol"; +import { IMetaEvidence } from "./IMetaEvidence.sol"; +import { IEscalatable } from "../interfaces/escalation/IEscalatable.sol"; +import { IEscalationResolver } from "../interfaces/escalation/IEscalationResolver.sol"; + +contract BosonKlerosConnector is IEscalatable, IArbitrable, IMetaEvidence { + error InvalidExchangeError(); + + struct BosonCase { + uint256 exchangeId; + uint256 buyerPercent; + uint256 sellerPercent; + // TODO: evidence data + } + + IArbitrator arbitrator; + IEscalationResolver escalationResolver; + + mapping(uint256 => BosonCase) klerosDisputeCases; + + constructor(IArbitrator _arbitrator, IEscalationResolver _escalationResolver) { + arbitrator = _arbitrator; + escalationResolver = _escalationResolver; + } + + function escalateDispute(uint256 _exchangeId, uint256 _buyerPercent, uint256 _sellerPercent) external { + // 3 is a number of ruling options. For example: 1 - buyer proposal, 2 - seller proposal, 3 - refuse to decide + uint256 disputeId = arbitrator.createDispute(3, ""); + klerosDisputeCases[disputeId] = BosonCase(_exchangeId, _buyerPercent, _sellerPercent); + } + + function escalationCost() external view returns (uint256 _cost) { + return arbitrator.arbitrationCost(""); + } + + /** + * @dev Give a ruling for a dispute. Must be called by the arbitrator. + * The purpose of this function is to ensure that the address calling it has the right to rule on the contract. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". + */ + function rule(uint256 _disputeID, uint256 _ruling) external { + BosonCase memory bosonCase = klerosDisputeCases[_disputeID]; + if (bosonCase.exchangeId == 0) revert InvalidExchangeError(); + + // Assume 3 is refusal to decide, 1 - buyer proposal is accepted, 2 - seller proposal is accepted + if (_ruling == 1) { + escalationResolver.decideDispute(bosonCase.exchangeId, bosonCase.buyerPercent); + } else if (_ruling == 2) { + escalationResolver.decideDispute(bosonCase.exchangeId, 10000 - bosonCase.sellerPercent); + } else { + escalationResolver.refuseEscalatedDispute(bosonCase.exchangeId); + } + + emit Ruling(arbitrator, _disputeID, _ruling); + } +} diff --git a/contracts/kleros/IArbitrable.sol b/contracts/kleros/IArbitrable.sol new file mode 100644 index 000000000..47824e1b1 --- /dev/null +++ b/contracts/kleros/IArbitrable.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8; + +import "./IArbitrator.sol"; + +/** + * @title IArbitrable + * Arbitrable interface. Note that this interface follows the ERC-792 standard. + * When developing arbitrable contracts, we need to: + * - Define the action taken when a ruling is received by the contract. + * - Allow dispute creation. For this a function must call arbitrator.createDispute{value: _fee}(_choices,_extraData); + */ +interface IArbitrable { + /** + * @dev To be raised when a ruling is given. + * @param _arbitrator The arbitrator giving the ruling. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _ruling The ruling which was given. + */ + event Ruling( + IArbitrator indexed _arbitrator, + uint256 indexed _disputeID, + uint256 _ruling + ); + + /** + * @dev Give a ruling for a dispute. Must be called by the arbitrator. + * The purpose of this function is to ensure that the address calling it has the right to rule on the contract. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". + */ + function rule(uint256 _disputeID, uint256 _ruling) external; +} diff --git a/contracts/kleros/IArbitrator.sol b/contracts/kleros/IArbitrator.sol new file mode 100644 index 000000000..86fc5e14e --- /dev/null +++ b/contracts/kleros/IArbitrator.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8; + +import "./IArbitrable.sol"; + +/** + * @title Arbitrator + * Arbitrator interface that implements the new arbitration standard. + * Unlike the ERC-792 this standard doesn't have anything related to appeals, so each arbitrator can implement an appeal system that suits it the most. + * When developing arbitrator contracts we need to: + * - Define the functions for dispute creation (createDispute). Don't forget to store the arbitrated contract and the disputeID (which should be unique, may nbDisputes). + * - Define the functions for cost display (arbitrationCost). + * - Allow giving rulings. For this a function must call arbitrable.rule(disputeID, ruling). + */ +interface IArbitrator { + /** + * @dev To be emitted when a dispute is created. + * @param _disputeID ID of the dispute. + * @param _arbitrable The contract which created the dispute. + */ + event DisputeCreation( + uint256 indexed _disputeID, + IArbitrable indexed _arbitrable + ); + + /** + * @dev Create a dispute. Must be called by the arbitrable contract. + * Must pay at least arbitrationCost(_extraData). + * @param _choices Amount of choices the arbitrator can make in this dispute. + * @param _extraData Can be used to give additional info on the dispute to be created. + * @return disputeID ID of the dispute created. + */ + function createDispute(uint256 _choices, bytes calldata _extraData) + external + payable + returns (uint256 disputeID); + + /** + * @dev Compute the cost of arbitration. It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. + * @param _extraData Can be used to give additional info on the dispute to be created. + * @return cost Required cost of arbitration. + */ + function arbitrationCost(bytes calldata _extraData) + external + view + returns (uint256 cost); +} diff --git a/contracts/kleros/IEscalatable.sol b/contracts/kleros/IEscalatable.sol new file mode 100644 index 000000000..7e542b870 --- /dev/null +++ b/contracts/kleros/IEscalatable.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8; + +interface IEscalatable { + function escalateDispute( + uint256 _exchangeId, + uint256 _buyerPercent, + uint256 _sellerPercent + ) external; + + function escalationCost() external returns (uint256 _cost); +} diff --git a/contracts/kleros/IEscalationResolver.sol b/contracts/kleros/IEscalationResolver.sol new file mode 100644 index 000000000..0e37c9dd5 --- /dev/null +++ b/contracts/kleros/IEscalationResolver.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8; + +interface IEscalationResolver { + function decideDispute(uint256 _exchangeId, uint256 _buyerPercent) external; + + function refuseEscalatedDispute(uint256 _exchangeId) external; +} diff --git a/contracts/kleros/IEvidence.sol b/contracts/kleros/IEvidence.sol new file mode 100644 index 000000000..7bf7f86b7 --- /dev/null +++ b/contracts/kleros/IEvidence.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IArbitrator.sol"; + +/** @title IEvidence + * ERC-1497: Evidence Standard + */ +interface IEvidence { + /** + * @dev To be raised when evidence is submitted. Should point to the resource (evidences are not to be stored on chain due to gas considerations). + * @param _arbitrator The arbitrator of the contract. + * @param _evidenceGroupID Unique identifier of the evidence group the evidence belongs to. + * @param _party The address of the party submiting the evidence. Note that 0x0 refers to evidence not submitted by any party. + * @param _evidence IPFS path to evidence, example: '/ipfs/Qmarwkf7C9RuzDEJNnarT3WZ7kem5bk8DZAzx78acJjMFH/evidence.json' + */ + event Evidence( + IArbitrator indexed _arbitrator, + uint256 indexed _evidenceGroupID, + address indexed _party, + string _evidence + ); +} diff --git a/contracts/kleros/IMetaEvidence.sol b/contracts/kleros/IMetaEvidence.sol new file mode 100644 index 000000000..f9e097edc --- /dev/null +++ b/contracts/kleros/IMetaEvidence.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IArbitrator.sol"; +import "./IEvidence.sol"; + +/** @title IEvidence + * ERC-1497: Evidence Standard + */ +interface IMetaEvidence is IEvidence { + /** + * @dev To be emitted when meta-evidence is submitted. + * @param _metaEvidenceID Unique identifier of meta-evidence. + * @param _evidence IPFS path to metaevidence, example: '/ipfs/Qmarwkf7C9RuzDEJNnarT3WZ7kem5bk8DZAzx78acJjMFH/metaevidence.json' + */ + event MetaEvidence(uint256 indexed _metaEvidenceID, string _evidence); + + /** + * @dev To be emitted when a dispute is created to link the correct meta-evidence to the disputeID. + * @param _arbitrator The arbitrator of the contract. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _metaEvidenceID Unique identifier of meta-evidence. + * @param _evidenceGroupID Unique identifier of the evidence group that is linked to this dispute. + */ + event Dispute( + IArbitrator indexed _arbitrator, + uint256 indexed _disputeID, + uint256 _metaEvidenceID, + uint256 _evidenceGroupID + ); +} diff --git a/contracts/protocol/facets/DisputeHandlerFacet.sol b/contracts/protocol/facets/DisputeHandlerFacet.sol index 48bc0b3da..9cd4e7bd9 100644 --- a/contracts/protocol/facets/DisputeHandlerFacet.sol +++ b/contracts/protocol/facets/DisputeHandlerFacet.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.9; +// CHANGES IN THIS CONTRACT ARE IMPLEMENTED FOR INOFRMATION PURPOSES AND SHOULD NOT BE USED IN PRODUCTION import "../../domain/BosonConstants.sol"; import { IBosonDisputeHandler } from "../../interfaces/handlers/IBosonDisputeHandler.sol"; @@ -9,12 +10,18 @@ import { DisputeBase } from "../bases/DisputeBase.sol"; import { FundsLib } from "../libs/FundsLib.sol"; import { EIP712Lib } from "../libs/EIP712Lib.sol"; +import { Address } from "../../ext_libs/Address.sol"; +import { IEscalationResolver } from "../../interfaces/escalation/IEscalationResolver.sol"; +import { IEscalatable } from "../../interfaces/escalation/IEscalatable.sol"; + /** * @title DisputeHandlerFacet * * @notice Handles disputes associated with exchanges within the protocol. */ -contract DisputeHandlerFacet is DisputeBase, IBosonDisputeHandler { +contract DisputeHandlerFacet is DisputeBase, IBosonDisputeHandler, IEscalationResolver { + using Address for address; + bytes32 private constant RESOLUTION_TYPEHASH = keccak256(bytes("Resolution(uint256 exchangeId,uint256 buyerPercentBasisPoints)")); // needed for verification during the resolveDispute @@ -107,12 +114,10 @@ contract DisputeHandlerFacet is DisputeBase, IBosonDisputeHandler { * @param _exchangeId - the id of the associated exchange * @param _newDisputeTimeout - new date when resolution period ends */ - function extendDisputeTimeout(uint256 _exchangeId, uint256 _newDisputeTimeout) - external - override - disputesNotPaused - nonReentrant - { + function extendDisputeTimeout( + uint256 _exchangeId, + uint256 _newDisputeTimeout + ) external override disputesNotPaused nonReentrant { // Verify that the caller is the seller. Get exchange -> get offer id -> get seller id -> get operator address and compare to msg.sender // Get the exchange, should be in disputed state (Exchange storage exchange, ) = getValidExchange(_exchangeId, ExchangeState.Disputed); @@ -226,20 +231,11 @@ contract DisputeHandlerFacet is DisputeBase, IBosonDisputeHandler { * - Dispute was escalated and escalation period has elapsed * * @param _exchangeId - the id of the associated exchange - * @param _buyerPercent - percentage of the pot that goes to the buyer - * @param _sigR - r part of the signer's signature - * @param _sigS - s part of the signer's signature - * @param _sigV - v part of the signer's signature + * @param _percent - percentage of the pot that goes to the buyer or seller */ - function resolveDispute( - uint256 _exchangeId, - uint256 _buyerPercent, - bytes32 _sigR, - bytes32 _sigS, - uint8 _sigV - ) external override disputesNotPaused nonReentrant { + function resolveDispute(uint256 _exchangeId, uint256 _percent) external override disputesNotPaused nonReentrant { // buyer should get at most 100% - require(_buyerPercent <= 10000, INVALID_BUYER_PERCENT); + require(_percent <= 10000, INVALID_BUYER_PERCENT); // Get the exchange, should be in disputed state (Exchange storage exchange, ) = getValidExchange(_exchangeId, ExchangeState.Disputed); @@ -261,38 +257,30 @@ contract DisputeHandlerFacet is DisputeBase, IBosonDisputeHandler { // get seller id to check if caller is the seller (bool exists, uint256 sellerId) = getSellerIdByOperator(msgSender()); - // variable to store who the expected signer is - address expectedSigner; - // find out if the caller is the seller or the buyer, and which address should be the signer if (exists && offer.sellerId == sellerId) { // caller is the seller - // get the buyer's address, which should be the signer of the resolution - (, Buyer storage buyer) = fetchBuyer(exchange.buyerId); - expectedSigner = buyer.wallet; + dispute.sellerPercent = _percent; + dispute.sellerPercentSet = true; } else { + // caller is the buyer uint256 buyerId; (exists, buyerId) = getBuyerIdByWallet(msgSender()); require(exists && buyerId == exchange.buyerId, NOT_BUYER_OR_SELLER); - // caller is the buyer - // get the seller's address, which should be the signer of the resolution - (, Seller storage seller, ) = fetchSeller(offer.sellerId); - expectedSigner = seller.operator; + dispute.buyerPercent = _percent; + dispute.buyerPercentSet = true; } - - // verify that the signature belongs to the expectedSigner - require( - EIP712Lib.verify(expectedSigner, hashResolution(_exchangeId, _buyerPercent), _sigR, _sigS, _sigV), - SIGNER_AND_SIGNATURE_DO_NOT_MATCH - ); } - // finalize the dispute - finalizeDispute(_exchangeId, exchange, dispute, disputeDates, DisputeState.Resolved, _buyerPercent); - - // Notify watchers of state change - emit DisputeResolved(_exchangeId, _buyerPercent, msgSender()); + if ( + dispute.buyerPercentSet && dispute.sellerPercentSet && dispute.sellerPercent + dispute.buyerPercent == 10000 + ) { + // finalize the dispute + finalizeDispute(_exchangeId, exchange, dispute, disputeDates, DisputeState.Resolved, dispute.buyerPercent); + // Notify watchers of state change + emit DisputeResolved(_exchangeId, dispute.buyerPercent, msgSender()); + } } /** @@ -344,6 +332,26 @@ contract DisputeHandlerFacet is DisputeBase, IBosonDisputeHandler { // make sure buyer sent enough funds to proceed FundsLib.validateIncomingPayment(offer.exchangeToken, disputeResolutionTerms.buyerEscalationDeposit); + // Fetch DR to check if Escalatable interface is implemented + (bool exists, DisputeResolver memory disputeResolver, ) = fetchDisputeResolver( + disputeResolutionTerms.disputeResolverId + ); + // Dispute resolver must already exist + require(exists, NO_SUCH_DISPUTE_RESOLVER); + + if (disputeResolver.operator.isContract()) { + require(dispute.buyerPercentSet, "At least buyer percent must be set"); + + IEscalatable escalatable = IEscalatable(disputeResolver.operator); + // check if the fee set in Boson Protocol is equal to the external escalation cost + require( + disputeResolutionTerms.feeAmount == escalatable.escalationCost(), + "DR Fee and external escalation cost must be equal" + ); + + escalatable.escalateDispute(_exchangeId, dispute.buyerPercent, dispute.sellerPercent); + } + // fetch the escalation period from the storage uint256 escalationResponsePeriod = disputeResolutionTerms.escalationResponsePeriod; @@ -375,12 +383,10 @@ contract DisputeHandlerFacet is DisputeBase, IBosonDisputeHandler { * @param _exchangeId - the id of the associated exchange * @param _buyerPercent - percentage of the pot that goes to the buyer */ - function decideDispute(uint256 _exchangeId, uint256 _buyerPercent) - external - override - disputesNotPaused - nonReentrant - { + function decideDispute( + uint256 _exchangeId, + uint256 _buyerPercent + ) external override(IBosonDisputeHandler, IEscalationResolver) disputesNotPaused nonReentrant { // Buyer should get at most 100% require(_buyerPercent <= 10000, INVALID_BUYER_PERCENT); @@ -411,7 +417,9 @@ contract DisputeHandlerFacet is DisputeBase, IBosonDisputeHandler { * * @param _exchangeId - the id of the associated exchange */ - function refuseEscalatedDispute(uint256 _exchangeId) external override disputesNotPaused nonReentrant { + function refuseEscalatedDispute( + uint256 _exchangeId + ) external override(IBosonDisputeHandler, IEscalationResolver) disputesNotPaused nonReentrant { // Make sure the dispute is valid and the caller is the dispute resolver (Exchange storage exchange, Dispute storage dispute, DisputeDates storage disputeDates) = disputeResolverChecks( _exchangeId @@ -503,16 +511,9 @@ contract DisputeHandlerFacet is DisputeBase, IBosonDisputeHandler { * @return dispute - the dispute details. See {BosonTypes.Dispute} * @return disputeDates - the dispute dates details {BosonTypes.DisputeDates} */ - function getDispute(uint256 _exchangeId) - external - view - override - returns ( - bool exists, - Dispute memory dispute, - DisputeDates memory disputeDates - ) - { + function getDispute( + uint256 _exchangeId + ) external view override returns (bool exists, Dispute memory dispute, DisputeDates memory disputeDates) { return fetchDispute(_exchangeId); } @@ -580,15 +581,9 @@ contract DisputeHandlerFacet is DisputeBase, IBosonDisputeHandler { * * @param _exchangeId - the id of the associated exchange */ - function disputeResolverChecks(uint256 _exchangeId) - internal - view - returns ( - Exchange storage exchange, - Dispute storage dispute, - DisputeDates storage disputeDates - ) - { + function disputeResolverChecks( + uint256 _exchangeId + ) internal view returns (Exchange storage exchange, Dispute storage dispute, DisputeDates storage disputeDates) { // Get the exchange, should be in disputed state (exchange, ) = getValidExchange(_exchangeId, ExchangeState.Disputed);