Skip to content

Commit d3bea2b

Browse files
Merge pull request #303 from unknownunknown1/master
fix(Linguo): add helpers and new event
2 parents 0e16401 + f7975fd commit d3bea2b

File tree

2 files changed

+214
-2
lines changed

2 files changed

+214
-2
lines changed

contracts/standard/arbitration/Linguo.sol

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @authors: [@unknownunknown1]
3-
* @reviewers: [@ferittuncer, @clesaege, @satello]
3+
* @reviewers: [@ferittuncer*, @clesaege*, @satello*]
44
* @auditors: []
55
* @bounties: []
66
* @deployments: []
@@ -73,6 +73,12 @@ contract Linguo is Arbitrable {
7373

7474
/* *** Events *** */
7575

76+
/** @dev To be emitted when the new task is created.
77+
* @param _taskID The ID of the newly created task.
78+
* @param _requester The address that created the task.
79+
*/
80+
event TaskCreated(uint indexed _taskID, address indexed _requester);
81+
7682
/** @dev To be emitted when a translation is submitted.
7783
* @param _taskID The ID of the respective task.
7884
* @param _translator The address that performed the translation.
@@ -199,6 +205,7 @@ contract Linguo is Arbitrable {
199205
task.requesterDeposit = msg.value;
200206

201207
emit MetaEvidence(taskID, _metaEvidence);
208+
emit TaskCreated(taskID, msg.sender);
202209
}
203210

204211
/** @dev Assigns a specific task to the sender. Requires a translator's deposit.
@@ -380,7 +387,7 @@ contract Linguo is Arbitrable {
380387
* @param _taskID The ID of the associated task.
381388
* @param _round The round from which to withdraw.
382389
*/
383-
function withdrawFeesAndRewards(address _beneficiary, uint _taskID, uint _round) external {
390+
function withdrawFeesAndRewards(address _beneficiary, uint _taskID, uint _round) public {
384391
Task storage task = tasks[_taskID];
385392
Round storage round = task.rounds[_round];
386393
require(task.status == Status.Resolved, "The task should be resolved.");
@@ -413,6 +420,18 @@ contract Linguo is Arbitrable {
413420
_beneficiary.send(reward); // It is the user responsibility to accept ETH.
414421
}
415422

423+
/** @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.
424+
* @param _beneficiary The address that made contributions.
425+
* @param _taskID The ID of the associated task.
426+
* @param _cursor The round from where to start withdrawing.
427+
* @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.
428+
*/
429+
function batchRoundWithdraw(address _beneficiary, uint _taskID, uint _cursor, uint _count) public {
430+
Task storage task = tasks[_taskID];
431+
for (uint i = _cursor; i<task.rounds.length && (_count==0 || i<_cursor+_count); i++)
432+
withdrawFeesAndRewards(_beneficiary, _taskID, i);
433+
}
434+
416435
/** @dev Gives a ruling for a dispute. Must be called by the arbitrator.
417436
* The purpose of this function is to ensure that the address calling it has the right to rule on the contract.
418437
* @param _disputeID ID of the dispute in the Arbitrator contract.
@@ -479,6 +498,38 @@ contract Linguo is Arbitrable {
479498
// * Getters * //
480499
// ******************** //
481500

501+
/** @dev Returns the sum of withdrawable wei from appeal rounds. This function is O(n), where n is the number of rounds of the task. This could exceed the gas limit, therefore this function should only be used for interface display and not by other contracts.
502+
* @param _taskID The ID of the associated task.
503+
* @param _beneficiary The contributor for which to query.
504+
* @return The total amount of wei available to withdraw.
505+
*/
506+
function amountWithdrawable(uint _taskID, address _beneficiary) external view returns (uint total){
507+
Task storage task = tasks[_taskID];
508+
if (task.status != Status.Resolved) return total;
509+
510+
for (uint i = 0; i < task.rounds.length; i++) {
511+
Round storage round = task.rounds[i];
512+
if (!round.hasPaid[uint(Party.Translator)] || !round.hasPaid[uint(Party.Challenger)]) {
513+
total += round.contributions[_beneficiary][uint(Party.Translator)] + round.contributions[_beneficiary][uint(Party.Challenger)];
514+
} else if (task.ruling == uint(Party.None)) {
515+
uint rewardTranslator = round.paidFees[uint(Party.Translator)] > 0
516+
? (round.contributions[_beneficiary][uint(Party.Translator)] * round.feeRewards) / (round.paidFees[uint(Party.Translator)] + round.paidFees[uint(Party.Challenger)])
517+
: 0;
518+
uint rewardChallenger = round.paidFees[uint(Party.Challenger)] > 0
519+
? (round.contributions[_beneficiary][uint(Party.Challenger)] * round.feeRewards) / (round.paidFees[uint(Party.Translator)] + round.paidFees[uint(Party.Challenger)])
520+
: 0;
521+
522+
total += rewardTranslator + rewardChallenger;
523+
} else {
524+
total += round.paidFees[uint(task.ruling)] > 0
525+
? (round.contributions[_beneficiary][uint(task.ruling)] * round.feeRewards) / round.paidFees[uint(task.ruling)]
526+
: 0;
527+
}
528+
}
529+
530+
return total;
531+
}
532+
482533
/** @dev Gets the deposit required for self-assigning the task.
483534
* @param _taskID The ID of the task.
484535
* @return deposit The translator's deposit.
@@ -509,6 +560,22 @@ contract Linguo is Arbitrable {
509560
}
510561
}
511562

563+
/** @dev Gets the total number of created tasks.
564+
* @return The number of created tasks.
565+
*/
566+
function getTaskCount() public view returns (uint) {
567+
return tasks.length;
568+
}
569+
570+
/** @dev Gets the number of rounds of the specific task.
571+
* @param _taskID The ID of the task.
572+
* @return The number of rounds.
573+
*/
574+
function getNumberOfRounds(uint _taskID) public view returns (uint) {
575+
Task storage task = tasks[_taskID];
576+
return task.rounds.length;
577+
}
578+
512579
/** @dev Gets the contributions made by a party for a given round of task's appeal.
513580
* @param _taskID The ID of the task.
514581
* @param _round The position of the round.

test/linguo.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,22 @@ contract('Linguo', function(accounts) {
132132
'TestMetaEvidence',
133133
'The event has wrong meta-evidence string'
134134
)
135+
136+
assert.equal(
137+
taskTx.logs[1].event,
138+
'TaskCreated',
139+
'The second event has not been created'
140+
)
141+
assert.equal(
142+
taskTx.logs[1].args._taskID.toNumber(),
143+
0,
144+
'The second event has wrong task ID'
145+
)
146+
assert.equal(
147+
taskTx.logs[1].args._requester,
148+
requester,
149+
'The second event has wrong requester address'
150+
)
135151
})
136152

137153
it('Should not be possible to deposit less than min price when creating a task', async () => {
@@ -1042,4 +1058,133 @@ contract('Linguo', function(accounts) {
10421058
'Incorrect balance of the crowdfunder after withdrawing'
10431059
)
10441060
})
1061+
1062+
it('Should correctly perform batch withdraw', async () => {
1063+
const requiredDeposit = (await linguo.getDepositValue(0)).toNumber()
1064+
1065+
await linguo.assignTask(0, {
1066+
from: translator,
1067+
value: requiredDeposit + 1e17
1068+
})
1069+
await linguo.submitTranslation(0, 'ipfs:/X', { from: translator })
1070+
1071+
const task = await linguo.tasks(0)
1072+
const price = task[6].toNumber()
1073+
// add a small amount because javascript can have small deviations up to several hundreds when operating with large numbers
1074+
const challengerDeposit =
1075+
arbitrationFee + (challengeMultiplier * price) / MULTIPLIER_DIVISOR + 1000
1076+
await linguo.challengeTranslation(0, {
1077+
from: challenger,
1078+
value: challengerDeposit
1079+
})
1080+
1081+
await arbitrator.giveRuling(0, 1)
1082+
1083+
const loserAppealFee =
1084+
arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR
1085+
1086+
await linguo.fundAppeal(0, 2, {
1087+
from: challenger,
1088+
value: loserAppealFee
1089+
})
1090+
1091+
const winnerAppealFee =
1092+
arbitrationFee + (arbitrationFee * winnerMultiplier) / MULTIPLIER_DIVISOR
1093+
1094+
await linguo.fundAppeal(0, 1, {
1095+
from: translator,
1096+
value: winnerAppealFee
1097+
})
1098+
const roundInfo = await linguo.getRoundInfo(0, 0)
1099+
1100+
await arbitrator.giveRuling(1, 1)
1101+
1102+
await linguo.fundAppeal(0, 2, {
1103+
from: challenger,
1104+
value: loserAppealFee
1105+
})
1106+
1107+
await linguo.fundAppeal(0, 1, {
1108+
from: translator,
1109+
value: winnerAppealFee
1110+
})
1111+
1112+
await arbitrator.giveRuling(2, 1)
1113+
1114+
await linguo.fundAppeal(0, 2, {
1115+
from: challenger,
1116+
value: 0.5 * loserAppealFee
1117+
})
1118+
1119+
await linguo.fundAppeal(0, 1, {
1120+
from: translator,
1121+
value: 0.5 * winnerAppealFee
1122+
})
1123+
1124+
await increaseTime(appealTimeOut + 1)
1125+
await arbitrator.giveRuling(2, 1)
1126+
1127+
const amountTranslator = await linguo.amountWithdrawable(0, translator)
1128+
const amountChallenger = await linguo.amountWithdrawable(0, challenger)
1129+
1130+
const oldBalanceTranslator = await web3.eth.getBalance(translator)
1131+
await linguo.batchRoundWithdraw(translator, 0, 1, 12, {
1132+
from: governor
1133+
})
1134+
const newBalanceTranslator1 = await web3.eth.getBalance(translator)
1135+
assert.equal(
1136+
newBalanceTranslator1.toString(),
1137+
oldBalanceTranslator
1138+
.plus(roundInfo[2])
1139+
.plus(0.5 * winnerAppealFee)
1140+
.toString(), // The last round was only paid half of the required amount.
1141+
'Incorrect translator balance after withdrawing from last 2 rounds'
1142+
)
1143+
await linguo.batchRoundWithdraw(translator, 0, 0, 1, {
1144+
from: governor
1145+
})
1146+
1147+
const newBalanceTranslator2 = await web3.eth.getBalance(translator)
1148+
assert.equal(
1149+
newBalanceTranslator2.toString(),
1150+
newBalanceTranslator1.plus(roundInfo[2]).toString(), // First 2 rounds have the same feeRewards value so we don't need to get info directly from each round.
1151+
'Incorrect translator balance after withdrawing from the first round'
1152+
)
1153+
1154+
// Check that 'amountWithdrawable' function returns the correct amount.
1155+
assert.equal(
1156+
newBalanceTranslator2.toString(),
1157+
oldBalanceTranslator.plus(amountTranslator).toString(),
1158+
'Getter function does not return correct withdrawable amount for translator'
1159+
)
1160+
1161+
const oldBalanceChallenger = await web3.eth.getBalance(challenger)
1162+
await linguo.batchRoundWithdraw(challenger, 0, 0, 2, {
1163+
from: governor
1164+
})
1165+
const newBalanceChallenger1 = await web3.eth.getBalance(challenger)
1166+
assert.equal(
1167+
newBalanceChallenger1.toString(),
1168+
oldBalanceChallenger.toString(),
1169+
'Challenger balance should stay the same after withdrawing from the first 2 rounds'
1170+
)
1171+
1172+
await linguo.batchRoundWithdraw(challenger, 0, 0, 20, {
1173+
from: governor
1174+
})
1175+
1176+
const newBalanceChallenger2 = await web3.eth.getBalance(challenger)
1177+
assert.equal(
1178+
newBalanceChallenger2.toString(),
1179+
newBalanceChallenger1.plus(0.5 * loserAppealFee).toString(),
1180+
'Incorrect challenger balance after withdrawing from the last round'
1181+
)
1182+
1183+
// Check that 'amountWithdrawable' function returns the correct amount.
1184+
assert.equal(
1185+
newBalanceChallenger2.toString(),
1186+
oldBalanceChallenger.plus(amountChallenger).toString(),
1187+
'Getter function does not return correct withdrawable amount for challenger'
1188+
)
1189+
})
10451190
})

0 commit comments

Comments
 (0)