diff --git a/contracts/standard/arbitration/MultipleArbitrableTransaction.sol b/contracts/standard/arbitration/MultipleArbitrableTransaction.sol new file mode 100644 index 00000000..7c531d7e --- /dev/null +++ b/contracts/standard/arbitration/MultipleArbitrableTransaction.sol @@ -0,0 +1,297 @@ +/** + * @title Multiple Arbitrable Transaction + * Bug Bounties: This code hasn't undertaken a bug bounty program yet. + */ + + +pragma solidity ^0.4.15; +import "./Arbitrator.sol"; + +/** @title Multiple Arbitrable Transaction + * This is a a contract for multiple arbitrated transactions which can be reversed by an arbitrator. + * This can be used for buying goods, services and for paying freelancers. + * Parties are identified as "seller" and "buyer". + */ + contract MultipleArbitrableTransaction { + string constant RULING_OPTIONS = "Reimburse buyer;Pay seller"; + + + + uint8 constant AMOUNT_OF_CHOICES = 2; + uint8 constant BUYER_WINS = 1; + uint8 constant SELLER_WINS = 2; + + enum Party {Seller, Buyer} + + enum Status {NoDispute, WaitingSeller, WaitingBuyer, DisputeCreated, Resolved} + + struct Transaction { + address seller; + address buyer; + uint256 amount; + uint256 timeout; // Time in seconds a party can take before being considered unresponding and lose the dispute. + uint disputeId; + Arbitrator arbitrator; + bytes arbitratorExtraData; + uint sellerFee; // Total fees paid by the seller. + uint buyerFee; // Total fees paid by the buyer. + uint lastInteraction; // Last interaction for the dispute procedure. + Status status; + } + + Transaction[] public transactions; + + mapping (bytes32 => uint) public disputeTxMap; + + + + /** @dev Constructor. + */ + function MultipleArbitrableTransaction() public { + } + + /** @dev To be raised when a dispute is created. The main purpose of this event is to let the arbitrator know the meaning ruling IDs. + * @param _transactionId The index of the transaction in dispute. + * @param _arbitrator The arbitrator of the contract. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _rulingOptions Map ruling IDs to short description of the ruling in a CSV format using ";" as a delimiter. Note that ruling IDs start a 1. For example "Send funds to buyer;Send funds to seller", means that ruling 1 will make the contract send funds to the buyer and 2 to the seller. + */ + event Dispute(uint indexed _transactionId, Arbitrator indexed _arbitrator, uint indexed _disputeID, string _rulingOptions); + + /** @dev To be raised when a ruling is given. + * @param _transactionId The index of the transaction in dispute. + * @param _arbitrator The arbitrator giving the ruling. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _ruling The ruling which was given. + */ + event Ruling(uint indexed _transactionId, Arbitrator indexed _arbitrator, uint indexed _disputeID, uint _ruling); + + /** @dev To be raised when evidence are submitted. Should point to the ressource (evidences are not to be stored on chain due to gas considerations). + * @param _arbitrator The arbitrator of the contract. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _party The address of the party submiting the evidence. Note that 0 is kept for evidences not submitted by any party. + * @param _evidence A link to evidence or if it is short the evidence itself. Can be web link ("http://X"), IPFS ("ipfs:/X") or another storing service (using the URI, see https://en.wikipedia.org/wiki/Uniform_Resource_Identifier ). One usecase of short evidence is to include the hash of the plain English contract. + */ + event Evidence(Arbitrator indexed _arbitrator, uint indexed _disputeID, address _party, string _evidence); + + /** @dev To be emmited at contract creation. Contains the hash of the plain text contract. This will allow any party to show what was the original contract. + * This event is used as cheap way of storing it. + * @param _transactionId The index of the transaction. + * @param _contractHash Keccak256 hash of the plain text contract. + */ + event ContractHash(uint indexed _transactionId, bytes32 _contractHash); + + /** @dev Indicate that a party has to pay a fee or would otherwise be considered as loosing. + * @param _transactionId The index of the transaction. + * @param _party The party who has to pay. + */ + event HasToPayFee(uint indexed _transactionId, Party _party); + + /** @dev Give 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. + * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". + */ + function rule(uint _disputeID, uint _ruling) public { + uint transactionId = disputeTxMap[keccak256(msg.sender,_disputeID)]; + Transaction storage transaction = transactions[transactionId]; + require(msg.sender==address(transaction.arbitrator)); + + emit Ruling(transactionId, Arbitrator(msg.sender),_disputeID,_ruling); + + executeRuling(_disputeID,_ruling); + } + + /** @dev Pay the arbitration fee to raise a dispute. To be called by the seller. UNTRUSTED. + * Note that the arbitrator can have createDispute throw, which will make this function throw and therefore lead to a party being timed-out. + * This is not a vulnerability as the arbitrator can rule in favor of one party anyway. + * @param _transactionId The index of the transaction. + */ + function payArbitrationFeeBySeller(uint _transactionId) payable { + Transaction storage transaction = transactions[_transactionId]; + require(msg.sender == transaction.seller); + + + uint arbitrationCost = transaction.arbitrator.arbitrationCost(transaction.arbitratorExtraData); + transaction.sellerFee += msg.value; + require(transaction.sellerFee >= arbitrationCost); // Require that the total pay at least the arbitration cost. + require(transaction.status < Status.DisputeCreated); // Make sure a dispute has not been created yet. + + transaction.lastInteraction = now; + if (transaction.buyerFee < arbitrationCost) { // The partyB still has to pay. This can also happens if he has paid, but arbitrationCost has increased. + transaction.status = Status.WaitingBuyer; + emit HasToPayFee(_transactionId, Party.Buyer); + } else { // The partyB has also paid the fee. We create the dispute + raiseDispute(_transactionId, arbitrationCost); + } + } + + /** @dev Pay the arbitration fee to raise a dispute. To be called by the buyer. UNTRUSTED. + * Note that this function mirror payArbitrationFeeBySeller. + * @param _transactionId The index of the transaction. + */ + function payArbitrationFeeByBuyer(uint _transactionId) payable { + Transaction storage transaction = transactions[_transactionId]; + require(msg.sender == transaction.buyer); + + uint arbitrationCost = transaction.arbitrator.arbitrationCost(transaction.arbitratorExtraData); + transaction.buyerFee += msg.value; + require(transaction.buyerFee >= arbitrationCost); // Require that the total pay at least the arbitration cost. + require(transaction.status < Status.DisputeCreated); // Make sure a dispute has not been created yet. + + transaction.lastInteraction = now; + if (transaction.sellerFee < arbitrationCost) { // The partyA still has to pay. This can also happens if he has paid, but arbitrationCost has increased. + transaction.status = Status.WaitingSeller; + emit HasToPayFee(_transactionId, Party.Seller); + } else { // The partyA has also paid the fee. We create the dispute + raiseDispute(_transactionId, arbitrationCost); + } + } + + /** @dev Create a dispute. UNTRUSTED. + * @param _transactionId The index of the transaction. + * @param _arbitrationCost Amount to pay the arbitrator. + */ + function raiseDispute(uint _transactionId, uint _arbitrationCost) internal { + Transaction storage transaction = transactions[_transactionId]; + transaction.status = Status.DisputeCreated; + transaction.disputeId = transaction.arbitrator.createDispute.value(_arbitrationCost)(AMOUNT_OF_CHOICES,transaction.arbitratorExtraData); + disputeTxMap[keccak256(transaction.arbitrator, transaction.disputeId)] = _transactionId; + emit Dispute(_transactionId, transaction.arbitrator, transaction.disputeId,RULING_OPTIONS); + } + + /** @dev Reimburse partyA if partyB fails to pay the fee. + * @param _transactionId The index of the transaction. + */ + function timeOutBySeller(uint _transactionId) { + Transaction storage transaction = transactions[_transactionId]; + require(msg.sender == transaction.seller); + + + require(transaction.status==Status.WaitingBuyer); + require(now>=transaction.lastInteraction+transaction.timeout); + + executeRuling(transaction.disputeId, SELLER_WINS); + } + + /** @dev Pay partyB if partyA fails to pay the fee. + * @param _transactionId The index of the transaction. + */ + function timeOutByBuyer(uint _transactionId) { + Transaction storage transaction = transactions[_transactionId]; + require(msg.sender == transaction.buyer); + + + require(transaction.status == Status.WaitingSeller); + require(now>=transaction.lastInteraction+transaction.timeout); + + executeRuling(transaction.disputeId,BUYER_WINS); + } + + /** @dev Submit a reference to evidence. EVENT. + * @param _transactionId The index of the transaction. + * @param _evidence A link to an evidence using its URI. + */ + function submitEvidence(uint _transactionId, string _evidence) { + Transaction storage transaction = transactions[_transactionId]; + require(msg.sender == transaction.buyer || msg.sender == transaction.seller); + + require(transaction.status>=Status.DisputeCreated); + emit Evidence(transaction.arbitrator,transaction.disputeId,msg.sender,_evidence); + } + + /** @dev Appeal an appealable ruling. + * Transfer the funds to the arbitrator. + * Note that no checks are required as the checks are done by the arbitrator. + * @param _transactionId The index of the transaction. + * @param _extraData Extra data for the arbitrator appeal procedure. + */ + function appeal(uint _transactionId, bytes _extraData) payable public { + Transaction storage transaction = transactions[_transactionId]; + require(msg.sender == transaction.buyer || msg.sender == transaction.seller); + + transaction.arbitrator.appeal.value(msg.value)(transaction.disputeId,_extraData); + } + + + + /** @dev + * @param _arbitrator The arbitrator of the contract. + * @param _hashContract Keccak hash of the plain English contract. + * @param _timeout Time after which a party automatically loose a dispute. + * @param _seller The recipient of the transaction. + * @param _arbitratorExtraData Extra data for the arbitrator. + */ + function createTransaction(Arbitrator _arbitrator, bytes32 _hashContract, uint _timeout, address _seller, bytes _arbitratorExtraData) payable public returns (uint transactionIndex) { + transactions.push(Transaction({ + seller: _seller, + buyer: msg.sender, + amount: msg.value, + timeout: _timeout, + arbitrator: _arbitrator, + arbitratorExtraData: _arbitratorExtraData, + disputeId: 0, + sellerFee: 0, + buyerFee: 0, + lastInteraction: now, + status: Status.NoDispute + })); + emit ContractHash(transactions.length - 1, _hashContract); + return transactions.length - 1; + } + + + /** @dev Transfer the transaction's amount to the seller if the timeout has passed + * @param _transactionId The index of the transaction. + */ + function withdraw(uint _transactionId) public { + Transaction storage transaction = transactions[_transactionId]; + require(msg.sender == transaction.seller); + require(now >= transaction.lastInteraction+transaction.timeout); + require(transaction.status == Status.NoDispute); + + transaction.seller.send(transaction.amount); + transaction.amount = 0; + + transaction.status = Status.Resolved; + } + + /** @dev Reimburse party A. To be called if the good or service can't be fully provided. + * @param _transactionId The index of the transaction. + * @param _amountReimbursed Amount to reimburse in wei. + */ + function reimburse(uint _transactionId, uint _amountReimbursed) public { + Transaction storage transaction = transactions[_transactionId]; + require(transaction.seller == msg.sender); + require(_amountReimbursed <= transaction.amount); + + transaction.buyer.transfer(_amountReimbursed); + transaction.amount -= _amountReimbursed; + } + + /** @dev Execute a ruling of a dispute. It reimburse the fee to the winning party. + * This need to be extended by contract inheriting from it. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _ruling Ruling given by the arbitrator. 1 : Reimburse the partyA. 2 : Pay the partyB. + */ + function executeRuling(uint _disputeID, uint _ruling) internal { + uint transactionId = disputeTxMap[keccak256(msg.sender,_disputeID)]; + Transaction storage transaction = transactions[transactionId]; + + require(_disputeID == transaction.disputeId); + require(_ruling <= AMOUNT_OF_CHOICES); + + // Give the arbitration fee back. + // Note that we use send to prevent a party from blocking the execution. + if (_ruling == SELLER_WINS) { + transaction.seller.send(transaction.sellerFee > transaction.buyerFee ? transaction.sellerFee : transaction.buyerFee); // In both cases sends the highest amount paid to avoid ETH to be stuck in the contract if the arbitrator lowers its fee. + transaction.seller.send(transaction.amount); + } else if (_ruling == BUYER_WINS) { + transaction.buyer.send(transaction.sellerFee > transaction.buyerFee ? transaction.sellerFee : transaction.buyerFee); + transaction.buyer.send(transaction.amount); + } + + transaction.amount = 0; + transaction.status = Status.Resolved; + } +} diff --git a/contracts/standard/arbitration/TwoPartyArbitrable.sol b/contracts/standard/arbitration/TwoPartyArbitrable.sol index d6fa019b..7893c921 100644 --- a/contracts/standard/arbitration/TwoPartyArbitrable.sol +++ b/contracts/standard/arbitration/TwoPartyArbitrable.sol @@ -63,7 +63,7 @@ contract TwoPartyArbitrable is Arbitrable { function payArbitrationFeeByPartyA() payable onlyPartyA { uint arbitrationCost=arbitrator.arbitrationCost(arbitratorExtraData); partyAFee+=msg.value; - require(partyAFee == arbitrationCost); // Require that the total pay at least the arbitration cost. + require(partyAFee >= arbitrationCost); // Require that the total pay at least the arbitration cost. require(status= arbitrationCost); // Require that the total pay at least the arbitration cost. require(status { + const handler = contractHashEvent.watch((error, result) => { + contractHashEvent.stopWatching(); + if (!error) { + resolve(result); + } else { + reject(error); + } + }); + }); + await callback(); + return await awaitable; + } + + it("Should handle 1 transaction", async () => { + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + 0x0, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + let payerBalanceBeforeReimbursment = web3.eth.getBalance(payer); + await multipleContract.reimburse(arbitrableTransactionId, 1000, { + from: payee + }); + let newPayerBalance = web3.eth.getBalance(payer); + let newContractBalance = web3.eth.getBalance(multipleContract.address); + let newAmount = (await multipleContract.transactions( + arbitrableTransactionId + ))[2]; + + assert.equal( + newPayerBalance.toString(), + payerBalanceBeforeReimbursment.plus(1000).toString(), + "The payer has not been reimbursed correctly" + ); + assert.equal( + newContractBalance.toNumber(), + 0, + "Bad amount in the contract" + ); + assert.equal(newAmount.toNumber(), 0, "Amount not updated correctly"); + }); + + it("Should handle 3 transaction", async () => { + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + for (var cnt = 0; cnt < 3; cnt += 1) { + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + 0x0, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + let payerBalanceBeforeReimbursment = web3.eth.getBalance(payer); + await multipleContract.reimburse(arbitrableTransactionId, 1000, { + from: payee + }); + let newPayerBalance = web3.eth.getBalance(payer); + let newContractBalance = web3.eth.getBalance(multipleContract.address); + let newAmount = (await multipleContract.transactions( + arbitrableTransactionId + ))[2]; + + assert.equal( + newPayerBalance.toString(), + payerBalanceBeforeReimbursment.plus(1000).toString(), + "The payer has not been reimbursed correctly" + ); + assert.equal( + newContractBalance.toNumber(), + 0, + "Bad amount in the contract" + ); + assert.equal(newAmount.toNumber(), 0, "Amount not updated correctly"); + } + }); + + it("Should put 1000 wei in the contract", async () => { + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + 0x0, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + assert.equal( + web3.eth.getBalance(multipleContract.address), + 1000, + "The contract hasn't received the wei correctly." + ); + let amountSending = (await multipleContract.transactions( + arbitrableTransactionId + ))[2]; + + assert.equal( + amountSending.toNumber(), + 1000, + "The contract hasn't updated its amount correctly." + ); + }); + + // Pay + it("The payee should withdraw", async () => { + let initialPayeeBalance = web3.eth.getBalance(payee); + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + 0x0, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + increaseTime(timeout + 1); + let tx = await multipleContract.withdraw(arbitrableTransactionId, { + from: payee + }); + let consumed = tx.receipt.gasUsed * 100000000000; + let newPayeeBalance = web3.eth.getBalance(payee); + assert.equal( + newPayeeBalance.toString(), + initialPayeeBalance.plus(1000 - consumed).toString(), + "The payee hasn't been paid properly" + ); + }); + + it("The payer should not withdraw", async () => { + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + 0x0, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + await expectThrow( + multipleContract.withdraw(arbitrableTransactionId, { from: payer }) + ); + }); + + // Reimburse + it("Should reimburse 507 to the payer", async () => { + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + 0x0, + contractHash, + 0 /* timeout */, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + let payerBalanceBeforeReimbursment = web3.eth.getBalance(payer); + await multipleContract.reimburse(arbitrableTransactionId, 507, { + from: payee + }); + let newPayerBalance = web3.eth.getBalance(payer); + let newContractBalance = web3.eth.getBalance(multipleContract.address); + let newAmount = (await multipleContract.transactions( + arbitrableTransactionId + ))[2]; + + assert.equal( + newPayerBalance.toString(), + payerBalanceBeforeReimbursment.plus(507).toString(), + "The payer has not been reimbursed correctly" + ); + assert.equal( + newContractBalance.toNumber(), + 493, + "Bad amount in the contract" + ); + assert.equal(newAmount.toNumber(), 493, "Amount not updated correctly"); + }); + + it("Should reimburse 1000 (all) to the payer", async () => { + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + 0x0, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + let payerBalanceBeforeReimbursment = web3.eth.getBalance(payer); + await multipleContract.reimburse(arbitrableTransactionId, 1000, { + from: payee + }); + let newPayerBalance = web3.eth.getBalance(payer); + let newContractBalance = web3.eth.getBalance(multipleContract.address); + let newAmount = (await multipleContract.transactions( + arbitrableTransactionId + ))[2]; + + assert.equal( + newPayerBalance.toString(), + payerBalanceBeforeReimbursment.plus(1000).toString(), + "The payer has not been reimbursed correctly" + ); + assert.equal( + newContractBalance.toNumber(), + 0, + "Bad amount in the contract" + ); + assert.equal(newAmount.toNumber(), 0, "Amount not updated correctly"); + }); + + it("Should fail if we try to reimburse more", async () => { + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + 0x0, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await expectThrow( + multipleContract.reimburse(arbitrableTransactionId, 1003, { from: payee }) + ); + }); + + it("Should fail if the payer to tries to reimburse it", async () => { + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + 0x0, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await expectThrow( + multipleContract.reimburse(arbitrableTransactionId, 1000, { from: payer }) + ); + }); + + // executeRuling + it("Should reimburse the payer (including arbitration fee) when the arbitrator decides so", async () => { + let centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId, { + from: payee, + value: arbitrationFee + }); + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId, { + from: payer, + value: arbitrationFee + }); + let payerBalanceBeforeReimbursment = web3.eth.getBalance(payer); + await centralizedArbitrator.giveRuling(0, 1, { from: arbitrator }); + let newPayerBalance = web3.eth.getBalance(payer); + assert.equal( + newPayerBalance.toString(), + payerBalanceBeforeReimbursment.plus(1020).toString(), + "The payer has not been reimbursed correctly" + ); + }); + + it("Should pay the payee and reimburse him the arbitration fee when the arbitrator decides so", async () => { + let centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId, { + from: payer, + value: arbitrationFee + }); + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId, { + from: payee, + value: arbitrationFee + }); + let payeeBalanceBeforePay = web3.eth.getBalance(payee); + await centralizedArbitrator.giveRuling(0, 2, { from: arbitrator }); + let newPayeeBalance = web3.eth.getBalance(payee); + assert.equal( + newPayeeBalance.toString(), + payeeBalanceBeforePay.plus(1020).toString(), + "The payee has not been paid properly" + ); + }); + + it("It should do nothing if the arbitrator decides so", async () => { + let centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId, { + from: payer, + value: arbitrationFee + }); + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId, { + from: payee, + value: arbitrationFee + }); + let payeeBalanceBeforePay = web3.eth.getBalance(payee); + let payerBalanceBeforeReimbursment = web3.eth.getBalance(payer); + await centralizedArbitrator.giveRuling(0, 0, { from: arbitrator }); + let newPayeeBalance = web3.eth.getBalance(payee); + let newPayerBalance = web3.eth.getBalance(payer); + assert.equal( + newPayeeBalance.toString(), + payeeBalanceBeforePay.toString(), + "The payee got wei while it shouldn't" + ); + assert.equal( + newPayerBalance.toString(), + payerBalanceBeforeReimbursment.toString(), + "The payer got wei while it shouldn't" + ); + }); + + it("Should reimburse the payer in case of timeout of the payee", async () => { + let centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId, { + from: payer, + value: arbitrationFee + }); + increaseTime(timeout + 1); + let payerBalanceBeforeReimbursment = web3.eth.getBalance(payer); + let tx = await multipleContract.timeOutByBuyer(arbitrableTransactionId, { + from: payer, + gasPrice: gasPrice + }); + let txFee = tx.receipt.gasUsed * gasPrice; + let newPayerBalance = web3.eth.getBalance(payer); + assert.equal( + newPayerBalance.toString(), + payerBalanceBeforeReimbursment + .plus(1020) + .minus(txFee) + .toString(), + "The payer has not been reimbursed correctly" + ); + }); + + it("Shouldn't work before timeout for the payer", async () => { + let centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await expectThrow( + multipleContract.timeOutByBuyer(arbitrableTransactionId, { + from: payer, + gasPrice: gasPrice + }) + ); + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId, { + from: payer, + value: arbitrationFee + }); + increaseTime(1); + await expectThrow( + multipleContract.timeOutByBuyer(arbitrableTransactionId, { + from: payer, + gasPrice: gasPrice + }) + ); + }); + + it("Should pay and reimburse the payee in case of timeout of the payer", async () => { + let centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId, { + from: payee, + value: arbitrationFee + }); + increaseTime(timeout + 1); + let payeeBalanceBeforeReimbursment = web3.eth.getBalance(payee); + let tx = await multipleContract.timeOutBySeller(arbitrableTransactionId, { + from: payee, + gasPrice: gasPrice + }); + let txFee = tx.receipt.gasUsed * gasPrice; + let newPayeeBalance = web3.eth.getBalance(payee); + assert.equal( + newPayeeBalance.toString(), + payeeBalanceBeforeReimbursment + .plus(1020) + .minus(txFee) + .toString(), + "The payee has not been paid correctly" + ); + }); + + it("Shouldn't work before timeout for the payee", async () => { + let centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await expectThrow( + multipleContract.timeOutBySeller(arbitrableTransactionId, { + from: payee, + gasPrice: gasPrice + }) + ); + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId, { + from: payee, + value: arbitrationFee + }); + increaseTime(1); + await expectThrow( + multipleContract.timeOutBySeller(arbitrableTransactionId, { + from: payee, + gasPrice: gasPrice + }) + ); + }); + + // submitEvidence + it("Should create events when evidence is submitted by the payer", async () => { + let centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId, { + from: payer, + value: arbitrationFee + }); + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId, { + from: payee, + value: arbitrationFee + }); + let tx = await multipleContract.submitEvidence( + arbitrableTransactionId, + "ipfs:/X", + { from: payer } + ); + assert.equal(tx.logs[0].event, "Evidence"); + assert.equal(tx.logs[0].args._arbitrator, centralizedArbitrator.address); + assert.equal(tx.logs[0].args._party, payer); + assert.equal(tx.logs[0].args._evidence, "ipfs:/X"); + }); + + it("Should create events when evidence is submitted by the payee", async () => { + let centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId, { + from: payer, + value: arbitrationFee + }); + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId, { + from: payee, + value: arbitrationFee + }); + let tx = await multipleContract.submitEvidence( + arbitrableTransactionId, + "ipfs:/X", + { from: payee } + ); + assert.equal(tx.logs[0].event, "Evidence"); + assert.equal(tx.logs[0].args._arbitrator, centralizedArbitrator.address); + assert.equal(tx.logs[0].args._party, payee); + assert.equal(tx.logs[0].args._evidence, "ipfs:/X"); + }); + + it("Should fail if someone else try to submit", async () => { + let centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + } + ); + let arbitrableTransactionId = lastTransaction.args._transactionId.toNumber(); + + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId, { + from: payer, + value: arbitrationFee + }); + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId, { + from: payee, + value: arbitrationFee + }); + await expectThrow( + multipleContract.submitEvidence(arbitrableTransactionId, "ipfs:/X", { + from: other + }) + ); + }); + + it("Should handle multiple transactions concurrently", async () => { + let centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const contractHashEvent = multipleContract.ContractHash(); + + let currentResolve; + let lastTransactionEvent = -1; + const handler = contractHashEvent.watch((error, result) => { + const eventTransaction = result.args._transactionId.toNumber(); + if (eventTransaction > lastTransactionEvent) { + lastTransactionEvent = eventTransaction; + currentResolve(result); + } + }); + + const transaction1Promise = new Promise(resolve => { + currentResolve = resolve; + + multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + }); + + const lastTransaction = await transaction1Promise; + + let arbitrableTransactionId1 = lastTransaction.args._transactionId.toNumber(); + + const transaction2Promise = new Promise(resolve => { + currentResolve = resolve; + + multipleContract.createTransaction( + centralizedArbitrator.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + }); + + const lastTransaction2 = await transaction2Promise; + + let arbitrableTransactionId2 = lastTransaction2.args._transactionId.toNumber(); + + contractHashEvent.stopWatching(); + + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId2, { + from: payer, + value: arbitrationFee + }); + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId1, { + from: payee, + value: arbitrationFee + }); + //This generates transaction 1 dispute 0 + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId1, { + from: payer, + value: arbitrationFee + }); + //This generates transaction 2 dispute 1 + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId2, { + from: payee, + value: arbitrationFee + }); + + let payerBalanceBeforeReimbursment = web3.eth.getBalance(payer); + //Ruling for transaction 1 + await centralizedArbitrator.giveRuling(0, 1, { from: arbitrator }); + let newPayerBalance = web3.eth.getBalance(payer); + assert.equal( + newPayerBalance.toString(), + payerBalanceBeforeReimbursment.plus(1020).toString(), + "The payer has not been reimbursed correctly" + ); + + let payeeBalanceBeforePay = web3.eth.getBalance(payee); + //ruling for transaction 2 + await centralizedArbitrator.giveRuling(1, 2, { from: arbitrator }); + let newPayeeBalance = web3.eth.getBalance(payee); + assert.equal( + newPayeeBalance.toString(), + payeeBalanceBeforePay.plus(1020).toString(), + "The payee has not been paid properly" + ); + }); + + it("Should handle multiple transactions and arbitrators concurrently", async () => { + let centralizedArbitrator1 = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ); + let centralizedArbitrator2 = await CentralizedArbitrator.new( + arbitrationFee, + { from: other } + ); + + let multipleContract = await MultipleArbitrableTransaction.new({ + from: payer + }); + + const contractHashEvent = multipleContract.ContractHash(); + + let currentResolve; + let lastTransactionEvent = -1; + const handler = contractHashEvent.watch((error, result) => { + const eventTransaction = result.args._transactionId.toNumber(); + if (eventTransaction > lastTransactionEvent) { + lastTransactionEvent = eventTransaction; + currentResolve(result); + } + }); + + const transaction1Promise = new Promise(resolve => { + currentResolve = resolve; + + multipleContract.createTransaction( + centralizedArbitrator1.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + }); + + const lastTransaction = await transaction1Promise; + + let arbitrableTransactionId1 = lastTransaction.args._transactionId.toNumber(); + + const transaction2Promise = new Promise(resolve => { + currentResolve = resolve; + + multipleContract.createTransaction( + centralizedArbitrator2.address, + contractHash, + timeout, + payee, + 0x0, + { from: payer, value: amount } + ); + }); + + const lastTransaction2 = await transaction2Promise; + + let arbitrableTransactionId2 = lastTransaction2.args._transactionId.toNumber(); + + contractHashEvent.stopWatching(); + + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId2, { + from: payer, + value: arbitrationFee + }); + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId1, { + from: payee, + value: arbitrationFee + }); + //This generates transaction 1 dispute 0 from arbitrator 1 + await multipleContract.payArbitrationFeeByBuyer(arbitrableTransactionId1, { + from: payer, + value: arbitrationFee + }); + //This generates transaction 2 dispute 0 from arbitrator 2 + await multipleContract.payArbitrationFeeBySeller(arbitrableTransactionId2, { + from: payee, + value: arbitrationFee + }); + + let payerBalanceBeforeReimbursment = web3.eth.getBalance(payer); + //Ruling for transaction 1 + await centralizedArbitrator1.giveRuling(0, 1, { from: arbitrator }); + let newPayerBalance = web3.eth.getBalance(payer); + assert.equal( + newPayerBalance.toString(), + payerBalanceBeforeReimbursment.plus(1020).toString(), + "The payer has not been reimbursed correctly" + ); + + let payeeBalanceBeforePay = web3.eth.getBalance(payee); + //ruling for transaction 2 + await centralizedArbitrator2.giveRuling(0, 2, { from: other }); + let newPayeeBalance = web3.eth.getBalance(payee); + assert.equal( + newPayeeBalance.toString(), + payeeBalanceBeforePay.plus(1020).toString(), + "The payee has not been paid properly" + ); + }); +});