diff --git a/contracts/standard/arbitration/Linguo.sol b/contracts/standard/arbitration/Linguo.sol index bc5e163c..dc92f28c 100644 --- a/contracts/standard/arbitration/Linguo.sol +++ b/contracts/standard/arbitration/Linguo.sol @@ -1,6 +1,6 @@ /** * @authors: [@unknownunknown1] - * @reviewers: [@ferittuncer, @clesaege, @satello] + * @reviewers: [@ferittuncer*, @clesaege*, @satello*] * @auditors: [] * @bounties: [] * @deployments: [] @@ -73,6 +73,12 @@ contract Linguo is Arbitrable { /* *** Events *** */ + /** @dev To be emitted when the new task is created. + * @param _taskID The ID of the newly created task. + * @param _requester The address that created the task. + */ + event TaskCreated(uint indexed _taskID, address indexed _requester); + /** @dev To be emitted when a translation is submitted. * @param _taskID The ID of the respective task. * @param _translator The address that performed the translation. @@ -199,6 +205,7 @@ contract Linguo is Arbitrable { task.requesterDeposit = msg.value; emit MetaEvidence(taskID, _metaEvidence); + emit TaskCreated(taskID, msg.sender); } /** @dev Assigns a specific task to the sender. Requires a translator's deposit. @@ -380,7 +387,7 @@ contract Linguo is Arbitrable { * @param _taskID The ID of the associated task. * @param _round The round from which to withdraw. */ - function withdrawFeesAndRewards(address _beneficiary, uint _taskID, uint _round) external { + function withdrawFeesAndRewards(address _beneficiary, uint _taskID, uint _round) public { Task storage task = tasks[_taskID]; Round storage round = task.rounds[_round]; require(task.status == Status.Resolved, "The task should be resolved."); @@ -413,6 +420,18 @@ contract Linguo is Arbitrable { _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 _beneficiary The address that made contributions. + * @param _taskID The ID of the associated task. + * @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. + */ + function batchRoundWithdraw(address _beneficiary, uint _taskID, uint _cursor, uint _count) public { + Task storage task = tasks[_taskID]; + for (uint i = _cursor; i 0 + ? (round.contributions[_beneficiary][uint(Party.Translator)] * round.feeRewards) / (round.paidFees[uint(Party.Translator)] + round.paidFees[uint(Party.Challenger)]) + : 0; + uint rewardChallenger = round.paidFees[uint(Party.Challenger)] > 0 + ? (round.contributions[_beneficiary][uint(Party.Challenger)] * round.feeRewards) / (round.paidFees[uint(Party.Translator)] + round.paidFees[uint(Party.Challenger)]) + : 0; + + total += rewardTranslator + rewardChallenger; + } else { + total += round.paidFees[uint(task.ruling)] > 0 + ? (round.contributions[_beneficiary][uint(task.ruling)] * round.feeRewards) / round.paidFees[uint(task.ruling)] + : 0; + } + } + + return total; + } + /** @dev Gets the deposit required for self-assigning the task. * @param _taskID The ID of the task. * @return deposit The translator's deposit. @@ -509,6 +560,22 @@ contract Linguo is Arbitrable { } } + /** @dev Gets the total number of created tasks. + * @return The number of created tasks. + */ + function getTaskCount() public view returns (uint) { + return tasks.length; + } + + /** @dev Gets the number of rounds of the specific task. + * @param _taskID The ID of the task. + * @return The number of rounds. + */ + function getNumberOfRounds(uint _taskID) public view returns (uint) { + Task storage task = tasks[_taskID]; + return task.rounds.length; + } + /** @dev Gets the contributions made by a party for a given round of task's appeal. * @param _taskID The ID of the task. * @param _round The position of the round. diff --git a/test/linguo.js b/test/linguo.js index eac92dbe..25b15a2b 100644 --- a/test/linguo.js +++ b/test/linguo.js @@ -132,6 +132,22 @@ contract('Linguo', function(accounts) { 'TestMetaEvidence', 'The event has wrong meta-evidence string' ) + + assert.equal( + taskTx.logs[1].event, + 'TaskCreated', + 'The second event has not been created' + ) + assert.equal( + taskTx.logs[1].args._taskID.toNumber(), + 0, + 'The second event has wrong task ID' + ) + assert.equal( + taskTx.logs[1].args._requester, + requester, + 'The second event has wrong requester address' + ) }) it('Should not be possible to deposit less than min price when creating a task', async () => { @@ -1042,4 +1058,133 @@ contract('Linguo', function(accounts) { 'Incorrect balance of the crowdfunder after withdrawing' ) }) + + it('Should correctly perform batch withdraw', async () => { + const requiredDeposit = (await linguo.getDepositValue(0)).toNumber() + + await linguo.assignTask(0, { + from: translator, + value: requiredDeposit + 1e17 + }) + await linguo.submitTranslation(0, 'ipfs:/X', { from: translator }) + + const task = await linguo.tasks(0) + const price = task[6].toNumber() + // add a small amount because javascript can have small deviations up to several hundreds when operating with large numbers + const challengerDeposit = + arbitrationFee + (challengeMultiplier * price) / MULTIPLIER_DIVISOR + 1000 + await linguo.challengeTranslation(0, { + from: challenger, + value: challengerDeposit + }) + + await arbitrator.giveRuling(0, 1) + + const loserAppealFee = + arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR + + await linguo.fundAppeal(0, 2, { + from: challenger, + value: loserAppealFee + }) + + const winnerAppealFee = + arbitrationFee + (arbitrationFee * winnerMultiplier) / MULTIPLIER_DIVISOR + + await linguo.fundAppeal(0, 1, { + from: translator, + value: winnerAppealFee + }) + const roundInfo = await linguo.getRoundInfo(0, 0) + + await arbitrator.giveRuling(1, 1) + + await linguo.fundAppeal(0, 2, { + from: challenger, + value: loserAppealFee + }) + + await linguo.fundAppeal(0, 1, { + from: translator, + value: winnerAppealFee + }) + + await arbitrator.giveRuling(2, 1) + + await linguo.fundAppeal(0, 2, { + from: challenger, + value: 0.5 * loserAppealFee + }) + + await linguo.fundAppeal(0, 1, { + from: translator, + value: 0.5 * winnerAppealFee + }) + + await increaseTime(appealTimeOut + 1) + await arbitrator.giveRuling(2, 1) + + const amountTranslator = await linguo.amountWithdrawable(0, translator) + const amountChallenger = await linguo.amountWithdrawable(0, challenger) + + const oldBalanceTranslator = await web3.eth.getBalance(translator) + await linguo.batchRoundWithdraw(translator, 0, 1, 12, { + from: governor + }) + const newBalanceTranslator1 = await web3.eth.getBalance(translator) + assert.equal( + newBalanceTranslator1.toString(), + oldBalanceTranslator + .plus(roundInfo[2]) + .plus(0.5 * winnerAppealFee) + .toString(), // The last round was only paid half of the required amount. + 'Incorrect translator balance after withdrawing from last 2 rounds' + ) + await linguo.batchRoundWithdraw(translator, 0, 0, 1, { + from: governor + }) + + const newBalanceTranslator2 = await web3.eth.getBalance(translator) + assert.equal( + newBalanceTranslator2.toString(), + 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. + 'Incorrect translator balance after withdrawing from the first round' + ) + + // Check that 'amountWithdrawable' function returns the correct amount. + assert.equal( + newBalanceTranslator2.toString(), + oldBalanceTranslator.plus(amountTranslator).toString(), + 'Getter function does not return correct withdrawable amount for translator' + ) + + const oldBalanceChallenger = await web3.eth.getBalance(challenger) + await linguo.batchRoundWithdraw(challenger, 0, 0, 2, { + from: governor + }) + const newBalanceChallenger1 = await web3.eth.getBalance(challenger) + assert.equal( + newBalanceChallenger1.toString(), + oldBalanceChallenger.toString(), + 'Challenger balance should stay the same after withdrawing from the first 2 rounds' + ) + + await linguo.batchRoundWithdraw(challenger, 0, 0, 20, { + from: governor + }) + + const newBalanceChallenger2 = await web3.eth.getBalance(challenger) + assert.equal( + newBalanceChallenger2.toString(), + newBalanceChallenger1.plus(0.5 * loserAppealFee).toString(), + 'Incorrect challenger balance after withdrawing from the last round' + ) + + // Check that 'amountWithdrawable' function returns the correct amount. + assert.equal( + newBalanceChallenger2.toString(), + oldBalanceChallenger.plus(amountChallenger).toString(), + 'Getter function does not return correct withdrawable amount for challenger' + ) + }) })