diff --git a/contracts/standard/arbitration/MultipleArbitrableTransactionWithFee.sol b/contracts/standard/arbitration/MultipleArbitrableTransactionWithFee.sol new file mode 100644 index 00000000..d96b7d80 --- /dev/null +++ b/contracts/standard/arbitration/MultipleArbitrableTransactionWithFee.sol @@ -0,0 +1,450 @@ +/** + * @authors: [@remedcu] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + * @tools: [MythX] + */ + +pragma solidity ^0.4.24; + +import "./Arbitrator.sol"; +import "./IArbitrable.sol"; + +contract MultipleArbitrableTransactionWithFee is IArbitrable { + + // **************************** // + // * Contract variables * // + // **************************** // + + uint8 constant AMOUNT_OF_CHOICES = 2; + uint8 constant SENDER_WINS = 1; + uint8 constant RECEIVER_WINS = 2; + + enum Party {Sender, Receiver} + enum Status {NoDispute, WaitingSender, WaitingReceiver, DisputeCreated, Resolved} + + struct Transaction { + address sender; + address receiver; + uint amount; + uint timeoutPayment; // Time in seconds after which the transaction can be automatically executed if not disputed. + uint disputeId; // If dispute exists, the ID of the dispute. + uint senderFee; // Total fees paid by the sender. + uint receiverFee; // Total fees paid by the receiver. + uint lastInteraction; // Last interaction for the dispute procedure. + Status status; + } + + address public feeRecipient; // Address which receives a % of receiver payment. + uint public feeRecipientBasisPoint; // The % of fee to be received by the feeRecipient, down to 2 decimal places as 550 = 5.5%. + Transaction[] public transactions; + bytes public arbitratorExtraData; // Extra data to set up the arbitration. + Arbitrator public arbitrator; // Address of the arbitrator contract. + uint public feeTimeout; // Time in seconds a party can take to pay arbitration fees before being considered unresponding and lose the dispute. + + + mapping (uint => uint) public disputeIDtoTransactionID; // One-to-one relationship between the dispute and the transaction. + + // **************************** // + // * Events * // + // **************************** // + + /** @dev To be emitted when a party pays or reimburses the other. + * @param _transactionID The index of the transaction. + * @param _amount The amount paid. + * @param _party The party that paid. + */ + event Payment(uint indexed _transactionID, uint _amount, address _party); + + /** @dev To be emitted when a fee is received by the feeRecipient. + * @param _transactionID The index of the transaction. + * @param _amount The amount paid. + */ + event FeeRecipientPayment(uint indexed _transactionID, uint _amount); + + /** @dev To be emitted when a feeRecipient is changed. + * @param _oldFeeRecipient Previous feeRecipient. + * @param _newFeeRecipient Current feeRecipient. + */ + event FeeRecipientChanged(address indexed _oldFeeRecipient, address indexed _newFeeRecipient); + + /** @dev Indicate that a party has to pay a fee or would otherwise be considered as losing. + * @param _transactionID The index of the transaction. + * @param _party The party who has to pay. + */ + event HasToPayFee(uint indexed _transactionID, Party _party); + + /** @dev To be raised when a ruling is given. + * @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(Arbitrator indexed _arbitrator, uint indexed _disputeID, uint _ruling); + + /** @dev Emitted when a transaction is created. + * @param _transactionID The index of the transaction. + * @param _sender The address of the sender. + * @param _receiver The address of the receiver. + * @param _amount The initial amount in the transaction. + */ + event TransactionCreated(uint _transactionID, address indexed _sender, address indexed _receiver, uint _amount); + + // **************************** // + // * Arbitrable functions * // + // * Modifying the state * // + // **************************** // + + /** @dev Constructor. + * @param _arbitrator The arbitrator of the contract. + * @param _arbitratorExtraData Extra data for the arbitrator. + * @param _feeRecipient Address which receives a % of receiver payment. + * @param _feeRecipientBasisPoint The % of fee to be received by the feeRecipient, down to 2 decimal places as 550 = 5.5%. + * @param _feeTimeout Arbitration fee timeout for the parties. + */ + constructor ( + Arbitrator _arbitrator, + bytes _arbitratorExtraData, + address _feeRecipient, + uint _feeRecipientBasisPoint, + uint _feeTimeout + ) public { + arbitrator = _arbitrator; + arbitratorExtraData = _arbitratorExtraData; + feeRecipient = _feeRecipient; + // Basis point being set higher than 10000 will result in underflow, but it's the responsibility of the deployer of the contract. + feeRecipientBasisPoint = _feeRecipientBasisPoint; + feeTimeout = _feeTimeout; + } + + /** @dev Create a transaction. + * @param _timeoutPayment Time after which a party can automatically execute the arbitrable transaction. + * @param _receiver The recipient of the transaction. + * @param _metaEvidence Link to the meta-evidence. + * @return transactionID The index of the transaction. + */ + function createTransaction( + uint _timeoutPayment, + address _receiver, + string _metaEvidence + ) public payable returns (uint transactionID) { + transactions.push(Transaction({ + sender: msg.sender, + receiver: _receiver, + amount: msg.value, + timeoutPayment: _timeoutPayment, + disputeId: 0, + senderFee: 0, + receiverFee: 0, + lastInteraction: now, + status: Status.NoDispute + })); + emit MetaEvidence(transactions.length - 1, _metaEvidence); + emit TransactionCreated(transactions.length - 1, msg.sender, _receiver, msg.value); + + return transactions.length - 1; + } + + /** @dev Calculate the amount to be paid in wei according to feeRecipientBasisPoint for a particular amount. + * @param _amount Amount to pay in wei. + */ + function calculateFeeRecipientAmount(uint _amount) internal view returns(uint feeAmount){ + feeAmount = (_amount * feeRecipientBasisPoint) / 10000; + } + + /** @dev Change Fee Recipient. + * @param _newFeeRecipient Address of the new Fee Recipient. + */ + function changeFeeRecipient(address _newFeeRecipient) public { + require(msg.sender == feeRecipient, "The caller must be the current Fee Recipient"); + feeRecipient = _newFeeRecipient; + + emit FeeRecipientChanged(msg.sender, _newFeeRecipient); + } + + /** @dev Pay receiver. To be called if the good or service is provided. + * @param _transactionID The index of the transaction. + * @param _amount Amount to pay in wei. + */ + function pay(uint _transactionID, uint _amount) public { + Transaction storage transaction = transactions[_transactionID]; + require(transaction.sender == msg.sender, "The caller must be the sender."); + require(transaction.status == Status.NoDispute, "The transaction shouldn't be disputed."); + require(_amount <= transaction.amount, "The amount paid has to be less than or equal to the transaction."); + + transaction.amount -= _amount; + + uint feeAmount = calculateFeeRecipientAmount(_amount); + feeRecipient.send(feeAmount); + transaction.receiver.send(_amount - feeAmount); + + emit Payment(_transactionID, _amount, msg.sender); + emit FeeRecipientPayment(_transactionID, feeAmount); + } + + /** @dev Reimburse sender. 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.receiver == msg.sender, "The caller must be the receiver."); + require(transaction.status == Status.NoDispute, "The transaction shouldn't be disputed."); + require(_amountReimbursed <= transaction.amount, "The amount reimbursed has to be less or equal than the transaction."); + + transaction.sender.transfer(_amountReimbursed); + transaction.amount -= _amountReimbursed; + emit Payment(_transactionID, _amountReimbursed, msg.sender); + } + + /** @dev Transfer the transaction's amount to the receiver if the timeout has passed. + * @param _transactionID The index of the transaction. + */ + function executeTransaction(uint _transactionID) public { + Transaction storage transaction = transactions[_transactionID]; + require(now - transaction.lastInteraction >= transaction.timeoutPayment, "The timeout has not passed yet."); + require(transaction.status == Status.NoDispute, "The transaction shouldn't be disputed."); + + uint amount = transaction.amount; + transaction.amount = 0; + uint feeAmount = calculateFeeRecipientAmount(amount); + feeRecipient.send(feeAmount); + transaction.receiver.send(amount - feeAmount); + + emit FeeRecipientPayment(_transactionID, feeAmount); + + transaction.status = Status.Resolved; + } + + /** @dev Reimburse sender if receiver fails to pay the fee. + * @param _transactionID The index of the transaction. + */ + function timeOutBySender(uint _transactionID) public { + Transaction storage transaction = transactions[_transactionID]; + require(transaction.status == Status.WaitingReceiver, "The transaction is not waiting on the receiver."); + require(now - transaction.lastInteraction >= feeTimeout, "Timeout time has not passed yet."); + + if (transaction.receiverFee != 0) { + transaction.receiver.send(transaction.receiverFee); + transaction.receiverFee = 0; + } + executeRuling(_transactionID, SENDER_WINS); + } + + /** @dev Pay receiver if sender fails to pay the fee. + * @param _transactionID The index of the transaction. + */ + function timeOutByReceiver(uint _transactionID) public { + Transaction storage transaction = transactions[_transactionID]; + require(transaction.status == Status.WaitingSender, "The transaction is not waiting on the sender."); + require(now - transaction.lastInteraction >= feeTimeout, "Timeout time has not passed yet."); + + if (transaction.senderFee != 0) { + transaction.sender.send(transaction.senderFee); + transaction.senderFee = 0; + } + executeRuling(_transactionID, RECEIVER_WINS); + } + + /** @dev Pay the arbitration fee to raise a dispute. To be called by the sender. 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 payArbitrationFeeBySender(uint _transactionID) public payable { + Transaction storage transaction = transactions[_transactionID]; + uint arbitrationCost = arbitrator.arbitrationCost(arbitratorExtraData); + + require(transaction.status < Status.DisputeCreated, "Dispute has already been created or because the transaction has been executed."); + require(msg.sender == transaction.sender, "The caller must be the sender."); + + transaction.senderFee += msg.value; + // Require that the total pay at least the arbitration cost. + require(transaction.senderFee >= arbitrationCost, "The sender fee must cover arbitration costs."); + + transaction.lastInteraction = now; + + // The receiver still has to pay. This can also happen if he has paid, but arbitrationCost has increased. + if (transaction.receiverFee < arbitrationCost) { + transaction.status = Status.WaitingReceiver; + emit HasToPayFee(_transactionID, Party.Receiver); + } else { // The receiver 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 receiver. UNTRUSTED. + * Note that this function mirrors payArbitrationFeeBySender. + * @param _transactionID The index of the transaction. + */ + function payArbitrationFeeByReceiver(uint _transactionID) public payable { + Transaction storage transaction = transactions[_transactionID]; + uint arbitrationCost = arbitrator.arbitrationCost(arbitratorExtraData); + + require(transaction.status < Status.DisputeCreated, "Dispute has already been created or because the transaction has been executed."); + require(msg.sender == transaction.receiver, "The caller must be the receiver."); + + transaction.receiverFee += msg.value; + // Require that the total paid to be at least the arbitration cost. + require(transaction.receiverFee >= arbitrationCost, "The receiver fee must cover arbitration costs."); + + transaction.lastInteraction = now; + // The sender still has to pay. This can also happen if he has paid, but arbitrationCost has increased. + if (transaction.senderFee < arbitrationCost) { + transaction.status = Status.WaitingSender; + emit HasToPayFee(_transactionID, Party.Sender); + } else { // The sender 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 = arbitrator.createDispute.value(_arbitrationCost)(AMOUNT_OF_CHOICES, arbitratorExtraData); + disputeIDtoTransactionID[transaction.disputeId] = _transactionID; + emit Dispute(arbitrator, transaction.disputeId, _transactionID, _transactionID); + + // Refund sender if it overpaid. + if (transaction.senderFee > _arbitrationCost) { + uint extraFeeSender = transaction.senderFee - _arbitrationCost; + transaction.senderFee = _arbitrationCost; + transaction.sender.send(extraFeeSender); + } + + // Refund receiver if it overpaid. + if (transaction.receiverFee > _arbitrationCost) { + uint extraFeeReceiver = transaction.receiverFee - _arbitrationCost; + transaction.receiverFee = _arbitrationCost; + transaction.receiver.send(extraFeeReceiver); + } + } + + /** @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) public { + Transaction storage transaction = transactions[_transactionID]; + require( + msg.sender == transaction.sender || msg.sender == transaction.receiver, + "The caller must be the sender or the receiver." + ); + require( + transaction.status < Status.Resolved, + "Must not send evidence if the dispute is resolved." + ); + + emit Evidence(arbitrator, _transactionID, 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. + */ + function appeal(uint _transactionID) public payable { + Transaction storage transaction = transactions[_transactionID]; + + arbitrator.appeal.value(msg.value)(transaction.disputeId, arbitratorExtraData); + } + + /** @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 = disputeIDtoTransactionID[_disputeID]; + Transaction storage transaction = transactions[transactionID]; + require(msg.sender == address(arbitrator), "The caller must be the arbitrator."); + require(transaction.status == Status.DisputeCreated, "The dispute has already been resolved."); + + emit Ruling(Arbitrator(msg.sender), _disputeID, _ruling); + + executeRuling(transactionID, _ruling); + } + + /** @dev Execute a ruling of a dispute. It reimburses the fee to the winning party. + * @param _transactionID The index of the transaction. + * @param _ruling Ruling given by the arbitrator. 1 : Reimburse the receiver. 2 : Pay the sender. + */ + function executeRuling(uint _transactionID, uint _ruling) internal { + Transaction storage transaction = transactions[_transactionID]; + require(_ruling <= AMOUNT_OF_CHOICES, "Invalid ruling."); + + uint amount = transaction.amount; + uint senderArbitrationFee = transaction.senderFee; + uint receiverArbitrationFee = transaction.receiverFee; + + transaction.amount = 0; + transaction.senderFee = 0; + transaction.receiverFee = 0; + + uint feeAmount; + + // Give the arbitration fee back. + // Note that we use send to prevent a party from blocking the execution. + if (_ruling == SENDER_WINS) { + transaction.sender.send(senderArbitrationFee + amount); + } else if (_ruling == RECEIVER_WINS) { + feeAmount = calculateFeeRecipientAmount(amount); + + feeRecipient.send(feeAmount); + transaction.receiver.send(receiverArbitrationFee + amount - feeAmount); + + emit FeeRecipientPayment(_transactionID, feeAmount); + } else { + uint split_arbitration = senderArbitrationFee / 2; + uint split_amount = amount / 2; + feeAmount = calculateFeeRecipientAmount(split_amount); + + transaction.sender.send(split_arbitration + split_amount); + feeRecipient.send(feeAmount); + transaction.receiver.send(split_arbitration + split_amount - feeAmount); + + emit FeeRecipientPayment(_transactionID, feeAmount); + } + + transaction.status = Status.Resolved; + } + + // **************************** // + // * Constant getters * // + // **************************** // + + /** @dev Getter to know the count of transactions. + * @return countTransactions The count of transactions. + */ + function getCountTransactions() public view returns (uint countTransactions) { + return transactions.length; + } + + /** @dev Get IDs for transactions where the specified address is the receiver and/or the sender. + * This function must be used by the UI and not by other smart contracts. + * Note that the complexity is O(t), where t is amount of arbitrable transactions. + * @param _address The specified address. + * @return transactionIDs The transaction IDs. + */ + function getTransactionIDsByAddress(address _address) public view returns (uint[] transactionIDs) { + uint count = 0; + for (uint i = 0; i < transactions.length; i++) { + if (transactions[i].sender == _address || transactions[i].receiver == _address) + count++; + } + + transactionIDs = new uint[](count); + + count = 0; + + for (uint j = 0; j < transactions.length; j++) { + if (transactions[j].sender == _address || transactions[j].receiver == _address) + transactionIDs[count++] = j; + } + } +} diff --git a/test/multiple-arbitrable-transaction-with-fee.js b/test/multiple-arbitrable-transaction-with-fee.js new file mode 100644 index 00000000..0a3d1e54 --- /dev/null +++ b/test/multiple-arbitrable-transaction-with-fee.js @@ -0,0 +1,1416 @@ +/* eslint-disable no-undef */ // Avoid the linter considering truffle elements as undef. +const { + expectThrow + } = require('openzeppelin-solidity/test/helpers/expectThrow') + const { + increaseTime + } = require('openzeppelin-solidity/test/helpers/increaseTime') + + const MultipleArbitrableTransactionWithFee = artifacts.require( + './MultipleArbitrableTransactionWithFee.sol' + ) + const CentralizedArbitrator = artifacts.require('./CentralizedArbitrator.sol') + + contract('MultipleArbitrableTransactionWithFee', function(accounts) { + const sender = accounts[0] + const receiver = accounts[1] + const arbitrator = accounts[2] + const other = accounts[3] + const feeRecipient = accounts[4] + const newFeeRecipient = accounts[5] + const feeRecipientBasisPoint = 500 + const feeTimeout = 100 + const timeoutPayment = 100 + const arbitrationFee = 20 + const newArbitrationFee = 62 + const gasPrice = 5000000000 + const metaEvidenceUri = 'https://kleros.io' + const amount = 1000 + const reimburse = 507 + + /** + * Getter for the last transaction + * @param {MultipleArbitrableTransactionWithFee} multipleContract Multiple arbitrable transaction instance. + * @param {function} callback The callback. + * @returns {function} The last transaction. + */ + async function getLastTransaction(multipleContract, callback) { + const metaEvidenceEvent = multipleContract.MetaEvidence() + const awaitable = new Promise((resolve, reject) => { + const _handler = metaEvidenceEvent.watch((error, result) => { + metaEvidenceEvent.stopWatching() + if (!error) resolve(result) + else reject(error) + }) + }) + await callback() + return awaitable + } + + function calculateFeeRecipientAmount(totalAmount) { + return (totalAmount/10000) * feeRecipientBasisPoint; + } + + it('Should handle 1 transaction', async () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + const senderBalanceBeforeReimbursment = web3.eth.getBalance(sender) + await multipleContract.reimburse(arbitrableTransactionId, amount, { + from: receiver + }) + const newSenderBalance = web3.eth.getBalance(sender) + const newContractBalance = web3.eth.getBalance(multipleContract.address) + const newAmount = (await multipleContract.transactions( + arbitrableTransactionId + ))[2] + + assert.equal( + newSenderBalance.toString(), + senderBalanceBeforeReimbursment.plus(amount).toString(), + 'The sender 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 emit TransactionCreated', async () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + const eventResult = await new Promise((resolve, reject) => { + multipleContract + .TransactionCreated({}, { fromBlock: 0, toBlock: 'latest' }) + .get((error, eventResult) => { + if (error) + reject(new Error('Could not lookup TransactionCreated event log')) + else resolve(eventResult) + }) + }) + + assert.equal(eventResult.length, 1) + assert.equal( + eventResult[0].args._transactionID.toNumber(), + arbitrableTransactionId + ) + assert.equal(eventResult[0].args._sender, sender) + assert.equal(eventResult[0].args._receiver, receiver) + }) + + it('Should handle 3 transaction', async () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + for (var cnt = 0; cnt < 3; cnt += 1) { + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + const senderBalanceBeforeReimbursment = web3.eth.getBalance(sender) + await multipleContract.reimburse(arbitrableTransactionId, amount, { + from: receiver + }) + const newSenderBalance = web3.eth.getBalance(sender) + const newContractBalance = web3.eth.getBalance(multipleContract.address) + const newAmount = (await multipleContract.transactions( + arbitrableTransactionId + ))[2] + + assert.equal( + newSenderBalance.toString(), + senderBalanceBeforeReimbursment.plus(amount).toString(), + 'The sender 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 specified amount in wei in the contract', async () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + assert.equal( + web3.eth.getBalance(multipleContract.address), + amount, + "The contract hasn't received the wei correctly." + ) + const amountSending = (await multipleContract.transactions( + arbitrableTransactionId + ))[2] + + assert.equal( + amountSending.toNumber(), + amount, + "The contract hasn't updated its amount correctly." + ) + }) + + // Pay + it('The receiver should execute payment', async () => { + const initialReceiverBalance = web3.eth.getBalance(receiver) + const initialFeeRecipientBalance = web3.eth.getBalance(feeRecipient) + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await increaseTime(timeoutPayment + 1) + + const tx = await multipleContract.executeTransaction( + arbitrableTransactionId, + { + from: receiver + } + ) + + const consumed = tx.receipt.gasUsed * 100000000000 + const feeRecipientAmount = calculateFeeRecipientAmount(amount) + const newReceiverBalance = web3.eth.getBalance(receiver) + const newFeeRecipientBalance = web3.eth.getBalance(feeRecipient) + assert.equal( + newReceiverBalance.toString(), + initialReceiverBalance.plus(amount).minus(consumed + feeRecipientAmount).toString(), + "The receiver hasn't been paid properly" + ) + assert.equal( + newFeeRecipientBalance.toString(), + initialFeeRecipientBalance.plus(feeRecipientAmount).toString(), + "The fee recipient hasn't been paid properly" + ) + }) + + it('The sender should not withdraw', async () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + await expectThrow( + multipleContract.executeTransaction(arbitrableTransactionId, { + from: sender + }) + ) + }) + + // Reimburse + it('Should reimburse X out of the amount to the sender', async () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + 0, + { from: sender } + ) + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + const senderBalanceBeforeReimbursment = web3.eth.getBalance(sender) + await multipleContract.reimburse(arbitrableTransactionId, 507, { + from: receiver + }) + const newSenderBalance = web3.eth.getBalance(sender) + const newContractBalance = web3.eth.getBalance(multipleContract.address) + const newAmount = (await multipleContract.transactions( + arbitrableTransactionId + ))[2] + + assert.equal( + newSenderBalance.toString(), + senderBalanceBeforeReimbursment.plus(reimburse).toString(), + 'The sender has not been reimbursed correctly' + ) + assert.equal( + newContractBalance.toNumber(), + amount - reimburse, + 'Bad amount in the contract' + ) + assert.equal(newAmount.toNumber(), amount - reimburse, 'Amount not updated correctly') + }) + + it('Should reimburse complete amount (all) to the sender', async () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + 0, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + const senderBalanceBeforeReimbursment = web3.eth.getBalance(sender) + await multipleContract.reimburse(arbitrableTransactionId, amount, { + from: receiver + }) + const newSenderBalance = web3.eth.getBalance(sender) + const newContractBalance = web3.eth.getBalance(multipleContract.address) + const newAmount = (await multipleContract.transactions( + arbitrableTransactionId + ))[2] + + assert.equal( + newSenderBalance.toString(), + senderBalanceBeforeReimbursment.plus(amount).toString(), + 'The sender 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 () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + 0, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await expectThrow( + multipleContract.reimburse(arbitrableTransactionId, amount + reimburse, { + from: receiver + }) + ) + }) + + it('Should fail if the sender tries to reimburse it', async () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + 0, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await expectThrow( + multipleContract.reimburse(arbitrableTransactionId, amount, { + from: sender + }) + ) + }) + + // executeRuling + it('Should reimburse the sender (including arbitration fee) when the arbitrator decides so', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + 0, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId, { + from: sender, + value: arbitrationFee + }) + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: arbitrationFee + } + ) + const senderBalanceBeforeReimbursment = web3.eth.getBalance(sender) + await centralizedArbitrator.giveRuling(0, 1, { from: arbitrator }) + const newSenderBalance = web3.eth.getBalance(sender) + assert.equal( + newSenderBalance.toString(), + senderBalanceBeforeReimbursment.plus(amount + arbitrationFee).toString(), + 'The sender has not been reimbursed correctly' + ) + }) + + it('Should pay the receiver and reimburse him the arbitration fee when the arbitrator decides so', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + 0, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: arbitrationFee + } + ) + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId, { + from: sender, + value: arbitrationFee + }) + + const receiverBalanceBeforePay = web3.eth.getBalance(receiver) + const feeRecipientBalanceBeforePay = web3.eth.getBalance(feeRecipient) + const feeRecipientAmount = calculateFeeRecipientAmount(amount) + + await centralizedArbitrator.giveRuling(0, 2, { from: arbitrator }) + const newReceiverBalance = web3.eth.getBalance(receiver) + const newFeeRecipientBalance = web3.eth.getBalance(feeRecipient) + + assert.equal( + newReceiverBalance.toString(), + receiverBalanceBeforePay.plus(amount + arbitrationFee - feeRecipientAmount).toString(), + 'The receiver has not been paid properly' + ) + assert.equal( + newFeeRecipientBalance.toString(), + feeRecipientBalanceBeforePay.plus(feeRecipientAmount).toString(), + "The fee recipient hasn't been paid properly" + ) + }) + + it('Should split the amount if there is no ruling', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + 0, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: arbitrationFee + } + ) + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId, { + from: sender, + value: arbitrationFee + }) + + const senderBalanceBeforeRuling = web3.eth.getBalance(sender) + const receiverBalanceBeforeRuling = web3.eth.getBalance(receiver) + const feeRecipientBalanceBeforeRuling = web3.eth.getBalance(feeRecipient) + const feeRecipientAmount = calculateFeeRecipientAmount(amount/2) + + await centralizedArbitrator.giveRuling(0, 0, { from: arbitrator }) + + const senderBalanceAfterRuling = web3.eth.getBalance(sender) + const receiverBalanceAfterRuling = web3.eth.getBalance(receiver) + const feeRecipientBalanceAfterRuling = web3.eth.getBalance(feeRecipient) + + assert.equal( + receiverBalanceAfterRuling.toString(), + receiverBalanceBeforeRuling.plus((amount/2) + (arbitrationFee/2) - feeRecipientAmount).toString(), + 'The receiver has not been reimbursed correctly' + ) + + assert.equal( + senderBalanceAfterRuling.toString(), + senderBalanceBeforeRuling.plus((amount/2) + (arbitrationFee/2)).toString(), + 'The sender has not been paid properly' + ) + + assert.equal( + feeRecipientBalanceAfterRuling.toString(), + feeRecipientBalanceBeforeRuling.plus(feeRecipientAmount).toString(), + "The fee recipient hasn't been paid properly" + ) + }) + + it('Should refund overpaid arbitration fee for sender', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + 0, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + const extraAmount = 100 + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: arbitrationFee + extraAmount + } + ) + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId, { + from: sender, + value: arbitrationFee + }) + const senderBalanceBeforePay = web3.eth.getBalance(sender) + await centralizedArbitrator.giveRuling(0, 2, { from: arbitrator }) + const newSenderBalance = web3.eth.getBalance(sender) + assert.equal( + newSenderBalance.toString(), + senderBalanceBeforePay.plus(0).toString(), + 'The sender was not refunded properly' + ) + }) + + it('Should change status to WaitingReceiver after the arbitration cost increase', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + 0, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: arbitrationFee + } + ) + + arbitrableTransactionStatus = (await multipleContract.transactions( + arbitrableTransactionId + ))[8] + + assert.equal( + arbitrableTransactionStatus.toNumber(), + 1, // `Status.WaitingSender == 1` + 'The transaction did not change correctly to new status: `Status.WaitingSender`' + ) + + await centralizedArbitrator.setArbitrationPrice(newArbitrationFee, { + from: arbitrator + }) + + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId, { + from: sender, + value: newArbitrationFee + }) + + arbitrableTransactionStatus = (await multipleContract.transactions( + arbitrableTransactionId + ))[8] + + assert.equal( + arbitrableTransactionStatus.toNumber(), + 2, // `Status.WaitingReceiver == 2` + 'The transaction did not change correctly to new status: `Status.WaitingReceiver`' + ) + }) + + it('Should split correclty the arbitration cost after the arbitration cost increase', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + 0, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: arbitrationFee + } + ) + + await centralizedArbitrator.setArbitrationPrice(newArbitrationFee, { + from: arbitrator + }) + + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId, { + from: sender, + value: newArbitrationFee + }) + + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: newArbitrationFee // Pay the rest of arbitration fee with an extra to test also the refund in this case + } + ) + + const senderBalanceBeforeRuling = web3.eth.getBalance(sender) + const receiverBalanceBeforeRuling = web3.eth.getBalance(receiver) + const feeRecipientBalanceBeforeRuling = web3.eth.getBalance(feeRecipient) + const feeRecipientAmount = calculateFeeRecipientAmount(amount/2) + + await centralizedArbitrator.giveRuling(0, 0, { from: arbitrator }) + + const senderBalanceAfterRuling = web3.eth.getBalance(sender) + const receiverBalanceAfterRuling = web3.eth.getBalance(receiver) + const feeRecipientBalanceAfterRuling = web3.eth.getBalance(feeRecipient) + + assert.equal( + receiverBalanceAfterRuling.toString(), + receiverBalanceBeforeRuling + .plus((amount/2) - feeRecipientAmount) + .plus(newArbitrationFee/2) + .toString(), + 'The receiver has not been reimbursed correctly' + ) + + assert.equal( + senderBalanceAfterRuling.toString(), + senderBalanceBeforeRuling + .plus((amount/2)) + .plus(newArbitrationFee/2) + .toString(), + 'The sender has not been paid properly' + ) + + assert.equal( + feeRecipientBalanceAfterRuling.toString(), + feeRecipientBalanceBeforeRuling.plus(feeRecipientAmount).toString(), + "The fee recipient hasn't been paid properly" + ) + + // check also the contract balance + assert.equal( + web3.eth.getBalance(multipleContract.address), + 0, + 'The ETH amount in the contract is not 0' + ) + }) + + it('Should reimburse the sender in case of timeout of the receiver', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + 0, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId, { + from: sender, + value: arbitrationFee + }) + await increaseTime(timeoutPayment + 1) + const senderBalanceBeforeReimbursment = web3.eth.getBalance(sender) + const tx = await multipleContract.timeOutBySender(arbitrableTransactionId, { + from: sender, + gasPrice: gasPrice + }) + const txFee = tx.receipt.gasUsed * gasPrice + const newSenderBalance = web3.eth.getBalance(sender) + assert.equal( + newSenderBalance.toString(), + senderBalanceBeforeReimbursment + .plus(amount + arbitrationFee) + .minus(txFee) + .toString(), + 'The sender has not been reimbursed correctly' + ) + }) + + it("Shouldn't work before timeout for the sender", async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await expectThrow( + multipleContract.timeOutBySender(arbitrableTransactionId, { + from: sender, + gasPrice: gasPrice + }) + ) + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId, { + from: sender, + value: arbitrationFee + }) + await increaseTime(1) + await expectThrow( + multipleContract.timeOutBySender(arbitrableTransactionId, { + from: sender, + gasPrice: gasPrice + }) + ) + }) + + it('Should pay and reimburse the receiver in case of timeout of the sender', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: arbitrationFee + } + ) + await increaseTime(feeTimeout + 1) + const receiverBalanceBeforeReimbursment = web3.eth.getBalance(receiver) + const feeRecipientBalanceBeforeReimbursment = web3.eth.getBalance(feeRecipient) + const feeRecipientAmount = calculateFeeRecipientAmount(amount) + const tx = await multipleContract.timeOutByReceiver( + arbitrableTransactionId, + { + from: receiver, + gasPrice: gasPrice + } + ) + const txFee = tx.receipt.gasUsed * gasPrice + const newReceiverBalance = web3.eth.getBalance(receiver) + const newfeeRecipientBalance = web3.eth.getBalance(feeRecipient) + assert.equal( + newReceiverBalance.toString(), + receiverBalanceBeforeReimbursment + .plus(amount + arbitrationFee) + .minus(txFee + feeRecipientAmount) + .toString(), + 'The receiver has not been paid correctly' + ) + assert.equal( + newfeeRecipientBalance.toString(), + feeRecipientBalanceBeforeReimbursment.plus(feeRecipientAmount).toString(), + "The fee recipient hasn't been paid properly" + ) + }) + + it("Shouldn't work before timeout for the receiver", async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await expectThrow( + multipleContract.timeOutByReceiver(arbitrableTransactionId, { + from: receiver, + gasPrice: gasPrice + }) + ) + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: arbitrationFee + } + ) + await increaseTime(1) + await expectThrow( + multipleContract.timeOutByReceiver(arbitrableTransactionId, { + from: receiver, + gasPrice: gasPrice + }) + ) + }) + + // submitEvidence + it('Should create events when evidence is submitted by the sender', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: arbitrationFee + } + ) + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId, { + from: sender, + value: arbitrationFee + }) + const tx = await multipleContract.submitEvidence( + arbitrableTransactionId, + 'ipfs:/X', + { from: sender } + ) + assert.equal(tx.logs[0].event, 'Evidence') + assert.equal(tx.logs[0].args._arbitrator, centralizedArbitrator.address) + assert.equal(tx.logs[0].args._party, sender) + assert.equal(tx.logs[0].args._evidence, 'ipfs:/X') + }) + + it('Should create events when evidence is submitted by the receiver', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: arbitrationFee + } + ) + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId, { + from: sender, + value: arbitrationFee + }) + const tx = await multipleContract.submitEvidence( + arbitrableTransactionId, + 'ipfs:/X', + { from: receiver } + ) + assert.equal(tx.logs[0].event, 'Evidence') + assert.equal(tx.logs[0].args._arbitrator, centralizedArbitrator.address) + assert.equal(tx.logs[0].args._party, receiver) + assert.equal(tx.logs[0].args._evidence, 'ipfs:/X') + }) + + it('Should fail if someone else try to submit', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId, + { + from: receiver, + value: arbitrationFee + } + ) + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId, { + from: sender, + value: arbitrationFee + }) + await expectThrow( + multipleContract.submitEvidence(arbitrableTransactionId, 'ipfs:/X', { + from: other + }) + ) + }) + + it('Should handle multiple transactions concurrently', async () => { + const centralizedArbitrator = await CentralizedArbitrator.new( + arbitrationFee, + { from: arbitrator } + ) + + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + centralizedArbitrator.address, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const metaEvidenceEvent = multipleContract.MetaEvidence() + + let currentResolve + let lastTransactionEvent = -1 + metaEvidenceEvent.watch((_error, result) => { + const eventTransaction = result.args._metaEvidenceID.toNumber() + if (eventTransaction > lastTransactionEvent) { + lastTransactionEvent = eventTransaction + currentResolve(result) + } + }) + + const transaction1Promise = new Promise(resolve => { + currentResolve = resolve + + multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + }) + + const lastTransaction = await transaction1Promise + + const arbitrableTransactionId1 = lastTransaction.args._metaEvidenceID.toNumber() + + const transaction2Promise = new Promise(resolve => { + currentResolve = resolve + + multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + }) + + const lastTransaction2 = await transaction2Promise + + const arbitrableTransactionId2 = lastTransaction2.args._metaEvidenceID.toNumber() + + metaEvidenceEvent.stopWatching() + + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId2, + { + from: receiver, + value: arbitrationFee + } + ) + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId1, { + from: sender, + value: arbitrationFee + }) + // This generates transaction 1 dispute 0 + await multipleContract.payArbitrationFeeByReceiver( + arbitrableTransactionId1, + { + from: receiver, + value: arbitrationFee + } + ) + // This generates transaction 2 dispute 1 + await multipleContract.payArbitrationFeeBySender(arbitrableTransactionId2, { + from: sender, + value: arbitrationFee + }) + + const senderBalanceBeforeReimbursment = web3.eth.getBalance(sender) + // Ruling for transaction 1 + await centralizedArbitrator.giveRuling(0, 1, { from: arbitrator }) + const newSenderBalance = web3.eth.getBalance(sender) + assert.equal( + newSenderBalance.toString(), + senderBalanceBeforeReimbursment.plus(amount + arbitrationFee).toString(), + 'The sender has not been reimbursed correctly' + ) + + const receiverBalanceBeforePay = web3.eth.getBalance(receiver) + const feeRecipientBalanceBeforePay = web3.eth.getBalance(feeRecipient) + const feeRecipientAmount = calculateFeeRecipientAmount(amount) + // ruling for transaction 2 + await centralizedArbitrator.giveRuling(1, 2, { from: arbitrator }) + const newReceiverBalance = web3.eth.getBalance(receiver) + const newFeeRecipientBalance = web3.eth.getBalance(feeRecipient) + assert.equal( + newReceiverBalance.toString(), + receiverBalanceBeforePay.plus(amount + arbitrationFee - feeRecipientAmount).toString(), + 'The receiver has not been paid properly' + ) + assert.equal( + newFeeRecipientBalance.toString(), + feeRecipientBalanceBeforePay.plus(feeRecipientAmount).toString(), + 'The receiver has not been paid properly' + ) + }) + + // newFeeRecipient + it('Should change to newFeeRecipient and emit the corresponding event', async () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + + const tx = await multipleContract.changeFeeRecipient( + newFeeRecipient, + { + from: feeRecipient + } + ) + + const _newFeeRecipient = await multipleContract.feeRecipient() + + assert.equal(tx.logs[0].event, 'FeeRecipientChanged') + assert.equal(tx.logs[0].args._oldFeeRecipient, feeRecipient) + assert.equal(tx.logs[0].args._newFeeRecipient, newFeeRecipient) + assert.equal(_newFeeRecipient, newFeeRecipient) + }) + + it('Only feeRecipient should be allowed to change to newFeeRecipient', async () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + + await expectThrow(multipleContract.changeFeeRecipient(newFeeRecipient, { from: other })) + }) + + // FeePayment + it('Should emit FeePayment', async () => { + const multipleContract = await MultipleArbitrableTransactionWithFee.new( + 0x0, + 0x0, + feeRecipient, + feeRecipientBasisPoint, + feeTimeout, + { from: sender } + ) + + const lastTransaction = await getLastTransaction( + multipleContract, + async () => { + await multipleContract.createTransaction( + timeoutPayment, + receiver, + metaEvidenceUri, + { from: sender, value: amount } + ) + } + ) + + const arbitrableTransactionId = lastTransaction.args._metaEvidenceID.toNumber() + + await increaseTime(timeoutPayment + 1) + + const tx = await multipleContract.executeTransaction( + arbitrableTransactionId, + { + from: receiver + } + ) + + assert.equal(tx.logs[0].event, 'FeeRecipientPayment') + assert.equal(tx.logs[0].args._transactionID.toNumber(), arbitrableTransactionId) + assert.equal(tx.logs[0].args._amount, calculateFeeRecipientAmount(amount)) + }) + + }) + \ No newline at end of file