Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions contracts/standard/arbitration/Linguo.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @authors: [@unknownunknown1]
* @reviewers: [@ferittuncer, @clesaege, @satello]
* @reviewers: [@ferittuncer*, @clesaege*, @satello*]
* @auditors: []
* @bounties: []
* @deployments: []
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.");
Expand Down Expand Up @@ -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<task.rounds.length && (_count==0 || i<_cursor+_count); i++)
withdrawFeesAndRewards(_beneficiary, _taskID, i);
}

/** @dev Gives 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.
Expand Down Expand Up @@ -479,6 +498,38 @@ contract Linguo is Arbitrable {
// * Getters * //
// ******************** //

/** @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.
* @param _taskID The ID of the associated task.
* @param _beneficiary The contributor for which to query.
* @return The total amount of wei available to withdraw.
*/
function amountWithdrawable(uint _taskID, address _beneficiary) external view returns (uint total){
Task storage task = tasks[_taskID];
if (task.status != Status.Resolved) return total;

for (uint i = 0; i < task.rounds.length; i++) {
Round storage round = task.rounds[i];
if (!round.hasPaid[uint(Party.Translator)] || !round.hasPaid[uint(Party.Challenger)]) {
total += round.contributions[_beneficiary][uint(Party.Translator)] + round.contributions[_beneficiary][uint(Party.Challenger)];
} else if (task.ruling == uint(Party.None)) {
uint rewardTranslator = round.paidFees[uint(Party.Translator)] > 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.
Expand Down Expand Up @@ -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.
Expand Down
145 changes: 145 additions & 0 deletions test/linguo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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'
)
})
})