This repository has been archived by the owner on Jul 31, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
MultiOutcomeArbitrable.sol
452 lines (394 loc) · 22.2 KB
/
MultiOutcomeArbitrable.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
/**
* @authors: [@fnanni-0]
* @reviewers: [@epiqueras*]
* @auditors: []
* @bounties: []
*/
pragma solidity >=0.7;
import "@kleros/erc-792/contracts/IArbitrator.sol";
import "@kleros/ethereum-libraries/contracts/CappedMath.sol";
library MultiOutcomeArbitrable {
using CappedMath for uint256;
/* *** Contract variables *** */
uint256 private constant MAX_NO_OF_CHOICES = type(uint256).max;
uint256 private constant MULTIPLIER_DIVISOR = 10000; // Divisor parameter for multipliers.
enum Status {None, Disputed, Resolved}
struct Round {
mapping(uint256 => uint256) paidFees; // Tracks the fees paid by each ruling in this round.
uint256 rulingFunded; // If the round is appealed, i.e. this is not the last round, 0 means that 2 rulings were fully funded.
uint256 totalFees; // Sum of fees paid during the funding of the appeal round.
uint256 appealCost; // Fees sent to the arbitrator in order to appeal.
mapping(address => mapping(uint256 => uint256)) contributions; // Maps contributors to their contributions for each ruling.
}
struct DisputeData {
mapping(uint256 => Round) rounds;
uint248 roundCounter;
Status status;
uint256 ruling;
uint256 disputeIDOnArbitratorSide;
}
struct ArbitrableStorage {
IArbitrator arbitrator; // Address of the arbitrator contract. Should only be set once. TRUSTED.
bytes arbitratorExtraData; // Extra data to set up the arbitration.
uint256 sharedStakeMultiplier; // Multiplier for calculating the appeal fee that must be paid by the submitter in the case where there is no winner or loser (e.g. when the arbitrator ruled "refuse to arbitrate").
uint256 winnerStakeMultiplier; // Multiplier for calculating the appeal fee of the party that won the previous round.
uint256 loserStakeMultiplier; // Multiplier for calculating the appeal fee of the party that lost the previous round.
mapping(uint256 => DisputeData) disputes; // disputes[localDisputeID]
mapping(uint256 => uint256) externalIDtoLocalID; // Maps external (arbitrator's side) dispute ids to local dispute ids. The local dispute ids must be defined by the arbitrable contract. externalIDtoLocalID[disputeIDOnArbitratorSide]
}
/* *** Events *** */
/// @dev When a library function emits an event, Solidity requires the event to be defined both inside the library and in the contract where the library is used. Make sure that your arbitrable contract inherits the interfaces mentioned below in order to comply with this (IArbitrable, IEvidence and IAppealEvents).
/// @dev See {@kleros/erc-792/contracts/IArbitrable.sol}.
event Ruling(IArbitrator indexed _arbitrator, uint256 indexed _disputeIDOnArbitratorSide, uint256 _ruling);
/// @dev See {@kleros/erc-792/contracts/erc-1497/IEvidence.sol}.
event Evidence(IArbitrator indexed _arbitrator, uint256 indexed _evidenceGroupID, address indexed _party, string _evidence);
/// @dev See {@kleros/erc-792/contracts/erc-1497/IEvidence.sol}.
event Dispute(IArbitrator indexed _arbitrator, uint256 indexed _disputeIDOnArbitratorSide, uint256 _metaEvidenceID, uint256 _evidenceGroupID);
/// @dev See {https://github.com/kleros/arbitrable-contract-libraries/blob/main/contracts/interfaces/IAppealEvents.sol}.
event HasPaidAppealFee(uint256 indexed _localDisputeID, uint256 _round, uint256 indexed _ruling);
/// @dev See {https://github.com/kleros/arbitrable-contract-libraries/blob/main/contracts/interfaces/IAppealEvents.sol}.
event AppealContribution(uint256 indexed _localDisputeID, uint256 _round, uint256 indexed _ruling, address indexed _contributor, uint256 _amount);
/// @dev See {https://github.com/kleros/arbitrable-contract-libraries/blob/main/contracts/interfaces/IAppealEvents.sol}.
event Withdrawal(uint256 indexed _localDisputeID, uint256 indexed _round, uint256 _ruling, address indexed _contributor, uint256 _reward);
// **************************** //
// * Modifying the state * //
// **************************** //
/** @dev Changes the stake multipliers.
* @param _sharedStakeMultiplier A new value of the multiplier for calculating appeal fees. In basis points.
* @param _winnerStakeMultiplier A new value of the multiplier for calculating appeal fees. In basis points.
* @param _loserStakeMultiplier A new value of the multiplier for calculating appeal fees. In basis points.
*/
function setMultipliers(
ArbitrableStorage storage self,
uint256 _sharedStakeMultiplier,
uint256 _winnerStakeMultiplier,
uint256 _loserStakeMultiplier
) internal {
self.sharedStakeMultiplier = _sharedStakeMultiplier;
self.winnerStakeMultiplier = _winnerStakeMultiplier;
self.loserStakeMultiplier = _loserStakeMultiplier;
}
/** @dev Sets the arbitrator data. Can only be set once.
* @param _arbitrator The address of the arbitrator contract the is going to be used for every dispute created.
* @param _arbitratorExtraData The extra data for the arbitrator.
*/
function setArbitrator(
ArbitrableStorage storage self,
IArbitrator _arbitrator,
bytes memory _arbitratorExtraData
) internal {
require(
self.arbitrator == IArbitrator(0x0) && _arbitrator != IArbitrator(0x0),
"Arbitrator is set or is invalid."
);
self.arbitrator = _arbitrator;
self.arbitratorExtraData = _arbitratorExtraData;
}
/** @dev Invokes the arbitrator to create a dispute. Requires _arbitrationCost ETH. It's the arbitrable contract responsability to make sure the amount of ETH available in the contract is enough.
* @param _localDisputeID The dispute ID as defined in the arbitrable contract.
* @param _arbitrationCost Value in wei, as defined in getArbitrationCost(), that is needed to create a dispute.
* @param _metaEvidenceID The ID of the meta-evidence of the dispute as defined in the ERC-1497 standard.
* @param _evidenceGroupID The ID of the evidence group the evidence belongs to.
* @return disputeID The ID assigned by the arbitrator to the newly created dispute.
*/
function createDispute(
ArbitrableStorage storage self,
uint256 _localDisputeID,
uint256 _arbitrationCost,
uint256 _metaEvidenceID,
uint256 _evidenceGroupID
) internal returns(uint256 disputeID) {
DisputeData storage dispute = self.disputes[_localDisputeID];
require(dispute.status == Status.None, "Dispute already created.");
disputeID = self.arbitrator.createDispute{value: _arbitrationCost}(MAX_NO_OF_CHOICES, self.arbitratorExtraData);
dispute.disputeIDOnArbitratorSide = disputeID;
dispute.status = Status.Disputed;
dispute.roundCounter = 1;
self.externalIDtoLocalID[disputeID] = _localDisputeID;
emit Dispute(self.arbitrator, disputeID, _metaEvidenceID, _evidenceGroupID);
}
/** @dev Submits a reference to evidence. EVENT.
* @param _localDisputeID The dispute ID as defined in the arbitrable contract.
* @param _evidenceGroupID ID of the evidence group the evidence belongs to. It must match the one used in createDispute().
* @param _evidence A link to evidence using its URI.
*/
function submitEvidence(
ArbitrableStorage storage self,
uint256 _localDisputeID,
uint256 _evidenceGroupID,
string memory _evidence
) internal {
require(
self.disputes[_localDisputeID].status < Status.Resolved,
"The dispute is resolved."
);
if (bytes(_evidence).length > 0)
emit Evidence(self.arbitrator, _evidenceGroupID, msg.sender, _evidence);
}
/** @dev Takes up to the total amount required to fund a side of an appeal. Reimburses the rest. Creates an appeal if two sides are fully funded.
* @param _localDisputeID The dispute ID as defined in the arbitrable contract.
* @param _ruling The ruling to which the contribution is made.
*/
function fundAppeal(ArbitrableStorage storage self, uint256 _localDisputeID, uint256 _ruling) internal {
DisputeData storage dispute = self.disputes[_localDisputeID];
require(dispute.status == Status.Disputed, "No ongoing dispute to appeal.");
uint256 currentRound = uint256(dispute.roundCounter - 1);
Round storage round = dispute.rounds[currentRound];
uint256 rulingFunded = round.rulingFunded; // Use local variable for gas saving purposes.
require(
_ruling != rulingFunded && _ruling != 0,
"Ruling is funded or is invalid."
);
(uint256 appealCost, uint256 totalCost) = getAppealFeeComponents(self, _localDisputeID, _ruling);
uint256 paidFee = round.paidFees[_ruling]; // Use local variable for gas saving purposes.
// Take up to the amount necessary to fund the current round at the current costs.
(uint256 contribution, uint256 remainingETH) = calculateContribution(msg.value, totalCost.subCap(paidFee));
round.contributions[msg.sender][_ruling] += contribution;
paidFee += contribution;
round.paidFees[_ruling] = paidFee;
round.totalFees += contribution; // Contributors to rulings that don't get fully funded can still win/lose rewards/contributions.
emit AppealContribution(_localDisputeID, currentRound, _ruling, msg.sender, contribution);
// Reimburse leftover ETH if any.
if (remainingETH > 0)
msg.sender.send(remainingETH); // Deliberate use of send in order not to block the contract in case of reverting fallback.
if (paidFee >= totalCost) {
emit HasPaidAppealFee(_localDisputeID, currentRound, _ruling);
if (rulingFunded == 0) {
round.rulingFunded = _ruling;
} else {
// Two rulings are fully funded. Create an appeal.
self.arbitrator.appeal{value: appealCost}(dispute.disputeIDOnArbitratorSide, self.arbitratorExtraData);
round.appealCost = appealCost;
round.rulingFunded = 0; // clear storage
dispute.roundCounter = uint248(currentRound + 2); // currentRound starts at 0 while roundCounter at 1.
}
}
}
/** @dev Validates and registers the ruling for a dispute. Can only 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. The ruling is inverted if a ruling loses from lack of appeal fees funding.
* @param _disputeIDOnArbitratorSide ID of the dispute in the Arbitrator contract.
* @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Refuse to arbitrate".
*/
function processRuling(
ArbitrableStorage storage self,
uint256 _disputeIDOnArbitratorSide,
uint256 _ruling
) internal returns(uint256 finalRuling) {
uint256 localDisputeID = self.externalIDtoLocalID[_disputeIDOnArbitratorSide];
DisputeData storage dispute = self.disputes[localDisputeID];
IArbitrator arbitrator = self.arbitrator;
require(
dispute.status == Status.Disputed &&
msg.sender == address(arbitrator),
"Ruling can't be processed."
);
Round storage round = dispute.rounds[dispute.roundCounter - 1];
// If only one ruling was fully funded, we consider it the winner, regardless of the arbitrator's decision.
if (round.rulingFunded == 0)
finalRuling = _ruling;
else
finalRuling = round.rulingFunded;
dispute.status = Status.Resolved;
dispute.ruling = finalRuling;
emit Ruling(arbitrator, _disputeIDOnArbitratorSide, finalRuling);
}
/** @dev Withdraws contributions of appeal rounds. Reimburses contributions if the appeal was not fully funded.
* If the appeal was fully funded, sends the fee stake rewards and reimbursements proportional to the contributions made to the winner of a dispute.
* @param _localDisputeID The dispute ID as defined in the arbitrable contract.
* @param _beneficiary The address that made contributions.
* @param _round The round from which to withdraw.
* @param _ruling The ruling to which the contributions were made.
*/
function withdrawFeesAndRewards(
ArbitrableStorage storage self,
uint256 _localDisputeID,
address payable _beneficiary,
uint256 _round,
uint256 _ruling
) internal {
DisputeData storage dispute = self.disputes[_localDisputeID];
require(dispute.status == Status.Resolved, "Dispute not resolved.");
uint256 reward = getWithdrawableAmount(self, _localDisputeID, _beneficiary, _round, _ruling);
if (reward > 0) {
dispute.rounds[_round].contributions[_beneficiary][_ruling] = 0;
emit Withdrawal(_localDisputeID, _round, _ruling, _beneficiary, reward);
_beneficiary.send(reward); // It is the user responsibility to accept ETH.
}
}
/** @dev Withdraws contributions of multiple appeal rounds at once. This function is O(n) where n is the number of rounds.
* This could exceed the gas limit, therefore this function should be used only as a utility and not be relied upon by other contracts.
* @param _localDisputeID The dispute ID as defined in the arbitrable contract.
* @param _beneficiary The address that made the contributions.
* @param _cursor The round from where to start withdrawing.
* @param _count The number of rounds to iterate. If set to 0 or a value larger than the number of rounds, iterates until the last round.
* @param _ruling The ruling to which the contributions were made.
*/
function batchWithdrawFeesAndRewards(
ArbitrableStorage storage self,
uint256 _localDisputeID,
address payable _beneficiary,
uint256 _cursor,
uint256 _count,
uint256 _ruling
) internal {
DisputeData storage dispute = self.disputes[_localDisputeID];
require(dispute.status == Status.Resolved, "Dispute not resolved.");
uint256 maxRound = _cursor + _count > dispute.roundCounter ? dispute.roundCounter : _cursor + _count;
uint256 reward;
for (uint256 i = _cursor; i < maxRound; i++) {
uint256 roundReward = getWithdrawableAmount(self, _localDisputeID, _beneficiary, i, _ruling);
reward += roundReward;
if (roundReward > 0) {
dispute.rounds[i].contributions[_beneficiary][_ruling] = 0;
emit Withdrawal(_localDisputeID, i, _ruling, _beneficiary, roundReward);
}
}
_beneficiary.send(reward); // It is the user responsibility to accept ETH.
}
// ******************** //
// * Getters * //
// ******************** //
/** @dev Gets the rewards withdrawable for a given round and ruling.
* Beware that withdrawals are allowed only after the dispute gets Resolved.
* @param _localDisputeID The dispute ID as defined in the arbitrable contract.
* @param _beneficiary The address that made the contributions.
* @param _round The round from which to withdraw.
* @param _ruling The ruling to which the contributions were made.
* @return reward The reward value to which the _beneficiary is entitled.
*/
function getWithdrawableAmount(
ArbitrableStorage storage self,
uint256 _localDisputeID,
address _beneficiary,
uint256 _round,
uint256 _ruling
) internal view returns(uint256 reward) {
DisputeData storage dispute = self.disputes[_localDisputeID];
Round storage round = dispute.rounds[_round];
uint256 lastRound = dispute.roundCounter - 1;
uint256 finalRuling = dispute.ruling;
mapping(uint256 => uint256) storage contributionTo = round.contributions[_beneficiary];
if (_round == lastRound) {
// Allow to reimburse if funding was unsuccessful, i.e. appeal wasn't created.
reward = contributionTo[_ruling];
} else if (round.paidFees[finalRuling] > 0) {
// If there is a winner, reward the winner.
if (_ruling == finalRuling) {
uint256 feeRewards = round.totalFees - round.appealCost;
reward = (contributionTo[_ruling] * feeRewards) / round.paidFees[_ruling];
}
} else {
// There is no winner. Reimburse unspent fees proportionally.
uint256 feeRewards = round.totalFees - round.appealCost;
reward = round.totalFees > 0 ? (contributionTo[_ruling] * feeRewards) / round.totalFees : 0;
}
}
/** @dev Returns the contribution value and remainder from available ETH and required amount.
* @param _available The amount of ETH available for the contribution.
* @param _requiredAmount The amount of ETH required for the contribution.
* @return taken The amount of ETH taken.
* @return remainder The amount of ETH left from the contribution.
*/
function calculateContribution(
uint256 _available,
uint256 _requiredAmount
) internal pure returns(uint256 taken, uint256 remainder) {
if (_requiredAmount > _available)
return (_available, 0); // Take whatever is available, return 0 as leftover ETH.
remainder = _available - _requiredAmount;
return (_requiredAmount, remainder);
}
/**
* @dev Calculates the appeal fee and total cost for an arbitration.
* @param _localDisputeID The dispute ID as defined in the arbitrable contract.
* @param _ruling The ruling to which the contribution is made.
* @return appealCost The appeal fee charged by the arbitrator. @return totalCost The total cost for appealing.
*/
function getAppealFeeComponents(
ArbitrableStorage storage self,
uint256 _localDisputeID,
uint256 _ruling
) internal view returns (uint256 appealCost, uint256 totalCost) {
DisputeData storage dispute = self.disputes[_localDisputeID];
IArbitrator arbitrator = self.arbitrator;
uint256 disputeIDOnArbitratorSide = dispute.disputeIDOnArbitratorSide;
(uint256 appealPeriodStart, uint256 appealPeriodEnd) = arbitrator.appealPeriod(disputeIDOnArbitratorSide);
require(block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, "Not in appeal period.");
uint256 multiplier;
uint256 winner = arbitrator.currentRuling(disputeIDOnArbitratorSide);
if (winner == _ruling){
multiplier = self.winnerStakeMultiplier;
} else if (winner == 0){
multiplier = self.sharedStakeMultiplier;
} else {
require(block.timestamp < (appealPeriodEnd + appealPeriodStart)/2, "Not in loser's appeal period.");
multiplier = self.loserStakeMultiplier;
}
appealCost = arbitrator.appealCost(disputeIDOnArbitratorSide, self.arbitratorExtraData);
totalCost = appealCost.addCap(appealCost.mulCap(multiplier) / MULTIPLIER_DIVISOR);
}
/** @dev Gets the final ruling if the dispute is resolved.
* @param _localDisputeID The dispute ID as defined in the arbitrable contract.
* @return The ruling that won the dispute.
*/
function getFinalRuling(ArbitrableStorage storage self, uint256 _localDisputeID) internal view returns(uint256) {
DisputeData storage dispute = self.disputes[_localDisputeID];
require(dispute.status == Status.Resolved, "Arbitrator has not ruled yet.");
return dispute.ruling;
}
/** @dev Gets the cost of arbitration using the given arbitrator and arbitratorExtraData.
* @return Arbitration cost.
*/
function getArbitrationCost(ArbitrableStorage storage self) internal view returns(uint256) {
return self.arbitrator.arbitrationCost(self.arbitratorExtraData);
}
/** @dev Gets the number of rounds of the specific dispute.
* @param _localDisputeID The dispute ID as defined in the arbitrable contract.
* @return The number of rounds.
*/
function getNumberOfRounds(ArbitrableStorage storage self, uint256 _localDisputeID) internal view returns (uint256) {
return self.disputes[_localDisputeID].roundCounter;
}
/** @dev Gets the information on a round of a disputed dispute.
* @param _localDisputeID The dispute ID as defined in the arbitrable contract.
* @param _round The round to be queried.
* @return rulingFunded feeRewards appealCostPaid appealed The round information.
*/
function getRoundInfo(
ArbitrableStorage storage self,
uint256 _localDisputeID,
uint256 _round
) internal view returns(
uint256 rulingFunded,
uint256 feeRewards,
uint256 appealCostPaid,
bool appealed
) {
DisputeData storage dispute = self.disputes[_localDisputeID];
Round storage round = dispute.rounds[_round];
rulingFunded = round.rulingFunded;
feeRewards = round.totalFees - round.appealCost;
appealCostPaid = round.appealCost;
appealed = _round != dispute.roundCounter - 1;
}
/** @dev Gets the contribution to a ruling made by an address for a given round of appeal of a dispute.
* @param _localDisputeID The dispute ID as defined in the arbitrable contract.
* @param _round The round number.
* @param _contributor The address of the contributor.
* @param _ruling The address of the contributor.
* @return contribution made by _contributor.
* @return rulingContributions sum of all contributions to _ruling.
*/
function getContribution(
ArbitrableStorage storage self,
uint256 _localDisputeID,
uint256 _round,
address _contributor,
uint256 _ruling
) internal view returns(uint256 contribution, uint256 rulingContributions) {
DisputeData storage dispute = self.disputes[_localDisputeID];
Round storage round = dispute.rounds[_round];
contribution = round.contributions[_contributor][_ruling];
rulingContributions = round.paidFees[_ruling];
}
}