|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | + |
| 3 | +pragma solidity ^0.8; |
| 4 | + |
| 5 | +import "./IArbitrator.sol"; |
| 6 | + |
| 7 | +/** @title Centralized Arbitrator |
| 8 | + * @dev This is a centralized arbitrator deciding alone on the result of disputes. It illustrates how IArbitrator interface can be implemented. |
| 9 | + * Note that this contract supports appeals. The ruling given by the arbitrator can be appealed by crowdfunding a desired choice. |
| 10 | + */ |
| 11 | +contract CentralizedArbitrator is IArbitrator { |
| 12 | + /* Constants */ |
| 13 | + |
| 14 | + // The required fee stake that a party must pay depends on who won the previous round and is proportional to the appeal cost such that the fee stake for a round is stake multiplier * appeal cost for that round. |
| 15 | + uint256 public constant WINNER_STAKE_MULTIPLIER = 10000; // Multiplier of the appeal cost that the winner has to pay as fee stake for a round in basis points. Default is 1x of appeal fee. |
| 16 | + uint256 public constant LOSER_STAKE_MULTIPLIER = 20000; // Multiplier of the appeal cost that the loser has to pay as fee stake for a round in basis points. Default is 2x of appeal fee. |
| 17 | + uint256 public constant LOSER_APPEAL_PERIOD_MULTIPLIER = 5000; // Multiplier of the appeal period for the choice that wasn't voted for in the previous round, in basis points. Default is 1/2 of original appeal period. |
| 18 | + uint256 public constant MULTIPLIER_DIVISOR = 10000; |
| 19 | + |
| 20 | + /* Enums */ |
| 21 | + |
| 22 | + enum DisputeStatus { |
| 23 | + Waiting, // The dispute is waiting for the ruling or not created. |
| 24 | + Appealable, // The dispute can be appealed. |
| 25 | + Solved // The dispute is resolved. |
| 26 | + } |
| 27 | + |
| 28 | + /* Structs */ |
| 29 | + |
| 30 | + struct DisputeStruct { |
| 31 | + IArbitrable arbitrated; // The address of the arbitrable contract. |
| 32 | + bytes arbitratorExtraData; // Extra data for the arbitrator. |
| 33 | + uint256 choices; // The number of choices the arbitrator can choose from. |
| 34 | + uint256 appealPeriodStart; // Time when the appeal funding becomes possible. |
| 35 | + uint256 arbitrationFee; // Fee paid by the arbitrable for the arbitration. Must be equal or higher than arbitration cost. |
| 36 | + uint256 ruling; // Ruling given by the arbitrator. |
| 37 | + DisputeStatus status; // A current status of the dispute. |
| 38 | + } |
| 39 | + |
| 40 | + struct Round { |
| 41 | + mapping(uint256 => uint256) paidFees; // Tracks the fees paid for each choice in this round. |
| 42 | + mapping(uint256 => bool) hasPaid; // True if this choice was fully funded, false otherwise. |
| 43 | + mapping(address => mapping(uint256 => uint256)) contributions; // Maps contributors to their contributions for each choice. |
| 44 | + uint256 feeRewards; // Sum of reimbursable appeal fees available to the parties that made contributions to the ruling that ultimately wins a dispute. |
| 45 | + uint256[] fundedChoices; // Stores the choices that are fully funded. |
| 46 | + } |
| 47 | + |
| 48 | + /* Storage */ |
| 49 | + |
| 50 | + address public owner = msg.sender; // Owner of the contract. |
| 51 | + uint256 public appealDuration; // The duration of the appeal period. |
| 52 | + |
| 53 | + uint256 private arbitrationFee; // The cost to create a dispute. Made private because of the arbitrationCost() getter. |
| 54 | + uint256 public appealFee; // The cost to fund one of the choices, not counting the additional fee stake amount. |
| 55 | + |
| 56 | + DisputeStruct[] public disputes; // Stores the dispute info. disputes[disputeID]. |
| 57 | + mapping(uint256 => Round[]) public disputeIDtoRoundArray; // Maps dispute IDs to Round array that contains the info about crowdfunding. |
| 58 | + |
| 59 | + /* Events */ |
| 60 | + |
| 61 | + /** |
| 62 | + * @dev To be emitted when a dispute can be appealed. |
| 63 | + * @param _disputeID ID of the dispute. |
| 64 | + * @param _arbitrable The contract which created the dispute. |
| 65 | + */ |
| 66 | + event AppealPossible(uint256 indexed _disputeID, IArbitrable indexed _arbitrable); |
| 67 | + |
| 68 | + /** |
| 69 | + * @dev To be emitted when the current ruling is appealed. |
| 70 | + * @param _disputeID ID of the dispute. |
| 71 | + * @param _arbitrable The contract which created the dispute. |
| 72 | + */ |
| 73 | + event AppealDecision(uint256 indexed _disputeID, IArbitrable indexed _arbitrable); |
| 74 | + |
| 75 | + /** @dev Raised when a contribution is made, inside fundAppeal function. |
| 76 | + * @param _disputeID ID of the dispute. |
| 77 | + * @param _round The round the contribution was made to. |
| 78 | + * @param _choice Indicates the choice option which got the contribution. |
| 79 | + * @param _contributor Caller of fundAppeal function. |
| 80 | + * @param _amount Contribution amount. |
| 81 | + */ |
| 82 | + event Contribution( |
| 83 | + uint256 indexed _disputeID, |
| 84 | + uint256 indexed _round, |
| 85 | + uint256 _choice, |
| 86 | + address indexed _contributor, |
| 87 | + uint256 _amount |
| 88 | + ); |
| 89 | + |
| 90 | + /** @dev Raised when a contributor withdraws a non-zero value. |
| 91 | + * @param _disputeID ID of the dispute. |
| 92 | + * @param _round The round the withdrawal was made from. |
| 93 | + * @param _choice Indicates the choice which contributor gets rewards from. |
| 94 | + * @param _contributor The beneficiary of the withdrawal. |
| 95 | + * @param _amount Total withdrawn amount, consists of reimbursed deposits and rewards. |
| 96 | + */ |
| 97 | + event Withdrawal( |
| 98 | + uint256 indexed _disputeID, |
| 99 | + uint256 indexed _round, |
| 100 | + uint256 _choice, |
| 101 | + address indexed _contributor, |
| 102 | + uint256 _amount |
| 103 | + ); |
| 104 | + |
| 105 | + /** @dev To be raised when a choice is fully funded for appeal. |
| 106 | + * @param _disputeID ID of the dispute. |
| 107 | + * @param _round ID of the round where the choice was funded. |
| 108 | + * @param _choice The choice that just got fully funded. |
| 109 | + */ |
| 110 | + event ChoiceFunded(uint256 indexed _disputeID, uint256 indexed _round, uint256 indexed _choice); |
| 111 | + |
| 112 | + /* Modifiers */ |
| 113 | + |
| 114 | + modifier onlyOwner() { |
| 115 | + require(msg.sender == owner, "Can only be called by the owner."); |
| 116 | + _; |
| 117 | + } |
| 118 | + |
| 119 | + /** @dev Constructor. |
| 120 | + * @param _arbitrationFee Amount to be paid for arbitration. |
| 121 | + * @param _appealDuration Duration of the appeal period. |
| 122 | + * @param _appealFee Amount to be paid to fund one of the appeal choices, not counting the additional fee stake amount. |
| 123 | + */ |
| 124 | + constructor( |
| 125 | + uint256 _arbitrationFee, |
| 126 | + uint256 _appealDuration, |
| 127 | + uint256 _appealFee |
| 128 | + ) public { |
| 129 | + arbitrationFee = _arbitrationFee; |
| 130 | + appealDuration = _appealDuration; |
| 131 | + appealFee = _appealFee; |
| 132 | + } |
| 133 | + |
| 134 | + /* External and Public */ |
| 135 | + |
| 136 | + /** @dev Set the arbitration fee. Only callable by the owner. |
| 137 | + * @param _arbitrationFee Amount to be paid for arbitration. |
| 138 | + */ |
| 139 | + function setArbitrationFee(uint256 _arbitrationFee) external onlyOwner { |
| 140 | + arbitrationFee = _arbitrationFee; |
| 141 | + } |
| 142 | + |
| 143 | + /** @dev Set the duration of the appeal period. Only callable by the owner. |
| 144 | + * @param _appealDuration New duration of the appeal period. |
| 145 | + */ |
| 146 | + function setAppealDuration(uint256 _appealDuration) external onlyOwner { |
| 147 | + appealDuration = _appealDuration; |
| 148 | + } |
| 149 | + |
| 150 | + /** @dev Set the appeal fee. Only callable by the owner. |
| 151 | + * @param _appealFee Amount to be paid for appeal. |
| 152 | + */ |
| 153 | + function setAppealFee(uint256 _appealFee) external onlyOwner { |
| 154 | + appealFee = _appealFee; |
| 155 | + } |
| 156 | + |
| 157 | + /** @dev Create a dispute. Must be called by the arbitrable contract. |
| 158 | + * Must be paid at least arbitrationCost(). |
| 159 | + * @param _choices Amount of choices the arbitrator can make in this dispute. |
| 160 | + * @param _extraData Can be used to give additional info on the dispute to be created. |
| 161 | + * @return disputeID ID of the dispute created. |
| 162 | + */ |
| 163 | + function createDispute(uint256 _choices, bytes calldata _extraData) |
| 164 | + external |
| 165 | + payable |
| 166 | + override |
| 167 | + returns (uint256 disputeID) |
| 168 | + { |
| 169 | + uint256 localArbitrationCost = arbitrationCost(_extraData); |
| 170 | + require(msg.value >= localArbitrationCost, "Not enough ETH to cover arbitration costs."); |
| 171 | + disputeID = disputes.length; |
| 172 | + disputes.push( |
| 173 | + DisputeStruct({ |
| 174 | + arbitrated: IArbitrable(msg.sender), |
| 175 | + arbitratorExtraData: _extraData, |
| 176 | + choices: _choices, |
| 177 | + appealPeriodStart: 0, |
| 178 | + arbitrationFee: msg.value, |
| 179 | + ruling: 0, |
| 180 | + status: DisputeStatus.Waiting |
| 181 | + }) |
| 182 | + ); |
| 183 | + |
| 184 | + disputeIDtoRoundArray[disputeID].push(); |
| 185 | + emit DisputeCreation(disputeID, IArbitrable(msg.sender)); |
| 186 | + } |
| 187 | + |
| 188 | + /** @dev TRUSTED. Manages contributions, and appeals a dispute if at least two choices are fully funded. This function allows the appeals to be crowdfunded. |
| 189 | + * Note that the surplus deposit will be reimbursed. |
| 190 | + * @param _disputeID Index of the dispute to appeal. |
| 191 | + * @param _choice A choice that receives funding. |
| 192 | + */ |
| 193 | + function fundAppeal(uint256 _disputeID, uint256 _choice) external payable { |
| 194 | + DisputeStruct storage dispute = disputes[_disputeID]; |
| 195 | + require(dispute.status == DisputeStatus.Appealable, "Dispute not appealable."); |
| 196 | + require(_choice <= dispute.choices, "There is no such ruling to fund."); |
| 197 | + |
| 198 | + (uint256 appealPeriodStart, uint256 appealPeriodEnd) = appealPeriod(_disputeID); |
| 199 | + require(block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, "Appeal period is over."); |
| 200 | + |
| 201 | + uint256 multiplier; |
| 202 | + if (dispute.ruling == _choice) { |
| 203 | + multiplier = WINNER_STAKE_MULTIPLIER; |
| 204 | + } else { |
| 205 | + require( |
| 206 | + block.timestamp - appealPeriodStart < |
| 207 | + ((appealPeriodEnd - appealPeriodStart) * LOSER_APPEAL_PERIOD_MULTIPLIER) / MULTIPLIER_DIVISOR, |
| 208 | + "Appeal period is over for loser" |
| 209 | + ); |
| 210 | + multiplier = LOSER_STAKE_MULTIPLIER; |
| 211 | + } |
| 212 | + |
| 213 | + Round[] storage rounds = disputeIDtoRoundArray[_disputeID]; |
| 214 | + uint256 lastRoundIndex = rounds.length - 1; |
| 215 | + Round storage lastRound = rounds[lastRoundIndex]; |
| 216 | + require(!lastRound.hasPaid[_choice], "Appeal fee is already paid."); |
| 217 | + |
| 218 | + uint256 totalCost = appealFee + (appealFee * multiplier) / MULTIPLIER_DIVISOR; |
| 219 | + |
| 220 | + // Take up to the amount necessary to fund the current round at the current costs. |
| 221 | + uint256 contribution; |
| 222 | + if (totalCost > lastRound.paidFees[_choice]) { |
| 223 | + contribution = totalCost - lastRound.paidFees[_choice] > msg.value // Overflows and underflows will be managed on the compiler level. |
| 224 | + ? msg.value |
| 225 | + : totalCost - lastRound.paidFees[_choice]; |
| 226 | + emit Contribution(_disputeID, lastRoundIndex, _choice, msg.sender, contribution); |
| 227 | + } |
| 228 | + |
| 229 | + lastRound.contributions[msg.sender][_choice] += contribution; |
| 230 | + lastRound.paidFees[_choice] += contribution; |
| 231 | + if (lastRound.paidFees[_choice] >= totalCost) { |
| 232 | + lastRound.feeRewards += lastRound.paidFees[_choice]; |
| 233 | + lastRound.fundedChoices.push(_choice); |
| 234 | + lastRound.hasPaid[_choice] = true; |
| 235 | + emit ChoiceFunded(_disputeID, lastRoundIndex, _choice); |
| 236 | + } |
| 237 | + |
| 238 | + if (lastRound.fundedChoices.length > 1) { |
| 239 | + // At least two sides are fully funded. |
| 240 | + rounds.push(); |
| 241 | + lastRound.feeRewards = lastRound.feeRewards - appealFee; |
| 242 | + |
| 243 | + dispute.status = DisputeStatus.Waiting; |
| 244 | + dispute.appealPeriodStart = 0; |
| 245 | + emit AppealDecision(_disputeID, dispute.arbitrated); |
| 246 | + } |
| 247 | + |
| 248 | + if (msg.value > contribution) payable(msg.sender).send(msg.value - contribution); |
| 249 | + } |
| 250 | + |
| 251 | + /** @dev Give a ruling to a dispute. Once it's given the dispute can be appealed, and after the appeal period has passed this function should be called again to finalize the ruling. |
| 252 | + * Accounts for the situation where the winner loses a case due to paying less appeal fees than expected. |
| 253 | + * @param _disputeID ID of the dispute to rule. |
| 254 | + * @param _ruling Ruling given by the arbitrator. Note that 0 means that arbitrator chose "Refused to rule". |
| 255 | + */ |
| 256 | + function giveRuling(uint256 _disputeID, uint256 _ruling) external onlyOwner { |
| 257 | + DisputeStruct storage dispute = disputes[_disputeID]; |
| 258 | + require(_ruling <= dispute.choices, "Invalid ruling."); |
| 259 | + require(dispute.status != DisputeStatus.Solved, "The dispute must not be solved."); |
| 260 | + |
| 261 | + if (dispute.status == DisputeStatus.Waiting) { |
| 262 | + dispute.ruling = _ruling; |
| 263 | + dispute.status = DisputeStatus.Appealable; |
| 264 | + dispute.appealPeriodStart = block.timestamp; |
| 265 | + emit AppealPossible(_disputeID, dispute.arbitrated); |
| 266 | + } else { |
| 267 | + require(block.timestamp > dispute.appealPeriodStart + appealDuration, "Appeal period not passed yet."); |
| 268 | + dispute.ruling = _ruling; |
| 269 | + dispute.status = DisputeStatus.Solved; |
| 270 | + |
| 271 | + Round[] storage rounds = disputeIDtoRoundArray[_disputeID]; |
| 272 | + Round storage lastRound = rounds[rounds.length - 1]; |
| 273 | + // If only one ruling option is funded, it wins by default. Note that if any other ruling had funded, an appeal would have been created. |
| 274 | + if (lastRound.fundedChoices.length == 1) { |
| 275 | + dispute.ruling = lastRound.fundedChoices[0]; |
| 276 | + } |
| 277 | + |
| 278 | + payable(msg.sender).send(dispute.arbitrationFee); // Avoid blocking. |
| 279 | + dispute.arbitrated.rule(_disputeID, dispute.ruling); |
| 280 | + } |
| 281 | + } |
| 282 | + |
| 283 | + /** @dev Allows to withdraw any reimbursable fees or rewards after the dispute gets resolved. |
| 284 | + * @param _disputeID Index of the dispute in disputes array. |
| 285 | + * @param _beneficiary The address which rewards to withdraw. |
| 286 | + * @param _round The round the caller wants to withdraw from. |
| 287 | + * @param _choice The ruling option that the caller wants to withdraw from. |
| 288 | + * @return amount The withdrawn amount. |
| 289 | + */ |
| 290 | + function withdrawFeesAndRewards( |
| 291 | + uint256 _disputeID, |
| 292 | + address payable _beneficiary, |
| 293 | + uint256 _round, |
| 294 | + uint256 _choice |
| 295 | + ) external returns (uint256 amount) { |
| 296 | + DisputeStruct storage dispute = disputes[_disputeID]; |
| 297 | + require(dispute.status == DisputeStatus.Solved, "Dispute should be resolved."); |
| 298 | + Round storage round = disputeIDtoRoundArray[_disputeID][_round]; |
| 299 | + |
| 300 | + if (!round.hasPaid[_choice]) { |
| 301 | + // Allow to reimburse if funding was unsuccessful for this ruling option. |
| 302 | + amount = round.contributions[_beneficiary][_choice]; |
| 303 | + } else { |
| 304 | + // Funding was successful for this ruling option. |
| 305 | + if (_choice == dispute.ruling) { |
| 306 | + // This ruling option is the ultimate winner. |
| 307 | + amount = round.paidFees[_choice] > 0 |
| 308 | + ? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice] |
| 309 | + : 0; |
| 310 | + } else if (!round.hasPaid[dispute.ruling]) { |
| 311 | + // The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed. |
| 312 | + amount = |
| 313 | + (round.contributions[_beneficiary][_choice] * round.feeRewards) / |
| 314 | + (round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]); |
| 315 | + } |
| 316 | + } |
| 317 | + round.contributions[_beneficiary][_choice] = 0; |
| 318 | + |
| 319 | + if (amount != 0) { |
| 320 | + _beneficiary.send(amount); // Deliberate use of send to prevent reverting fallback. It's the user's responsibility to accept ETH. |
| 321 | + emit Withdrawal(_disputeID, _round, _choice, _beneficiary, amount); |
| 322 | + } |
| 323 | + } |
| 324 | + |
| 325 | + // ************************ // |
| 326 | + // * Getters * // |
| 327 | + // ************************ // |
| 328 | + |
| 329 | + /** @dev Cost of arbitration. |
| 330 | + * @return fee The required amount. |
| 331 | + */ |
| 332 | + function arbitrationCost( |
| 333 | + bytes calldata /*_extraData*/ |
| 334 | + ) public view override returns (uint256 fee) { |
| 335 | + return arbitrationFee; |
| 336 | + } |
| 337 | + |
| 338 | + /** @dev Return the funded amount and funding goal for one of the choices. |
| 339 | + * @param _disputeID The ID of the dispute to appeal. |
| 340 | + * @param _choice The choice to check the funding status of. |
| 341 | + * @return funded The amount funded so far for this choice in wei. |
| 342 | + * @return goal The amount to fully fund this choice in wei. |
| 343 | + */ |
| 344 | + function fundingStatus(uint256 _disputeID, uint256 _choice) external view returns (uint256 funded, uint256 goal) { |
| 345 | + DisputeStruct storage dispute = disputes[_disputeID]; |
| 346 | + require(_choice <= dispute.choices, "There is no such ruling to fund."); |
| 347 | + require(dispute.status == DisputeStatus.Appealable, "Dispute not appealable."); |
| 348 | + |
| 349 | + if (dispute.ruling == _choice) { |
| 350 | + goal = appealFee + (appealFee * WINNER_STAKE_MULTIPLIER) / MULTIPLIER_DIVISOR; |
| 351 | + } else { |
| 352 | + goal = appealFee + (appealFee * LOSER_STAKE_MULTIPLIER) / MULTIPLIER_DIVISOR; |
| 353 | + } |
| 354 | + |
| 355 | + Round[] storage rounds = disputeIDtoRoundArray[_disputeID]; |
| 356 | + Round storage lastRound = rounds[rounds.length - 1]; |
| 357 | + |
| 358 | + return (lastRound.paidFees[_choice], goal); |
| 359 | + } |
| 360 | + |
| 361 | + /** @dev Compute the start and end of the dispute's appeal period, if possible. If the dispute is not appealble return (0, 0). |
| 362 | + * @param _disputeID ID of the dispute. |
| 363 | + * @return start The start of the period. |
| 364 | + * @return end The end of the period. |
| 365 | + */ |
| 366 | + function appealPeriod(uint256 _disputeID) public view returns (uint256 start, uint256 end) { |
| 367 | + DisputeStruct storage dispute = disputes[_disputeID]; |
| 368 | + if (dispute.status == DisputeStatus.Appealable) { |
| 369 | + start = dispute.appealPeriodStart; |
| 370 | + end = start + appealDuration; |
| 371 | + } |
| 372 | + return (start, end); |
| 373 | + } |
| 374 | +} |
0 commit comments