diff --git a/contracts/standard/permission/ArbitrableTokenList.sol b/contracts/standard/permission/ArbitrableTokenList.sol new file mode 100644 index 00000000..97da8ac3 --- /dev/null +++ b/contracts/standard/permission/ArbitrableTokenList.sol @@ -0,0 +1,673 @@ +/** + * @title ArbitrableTokenList + * @author Matheus Alencar - + * This code hasn't undertaken bug bounty programs yet. + */ + +pragma solidity ^0.4.24; + +import "../arbitration/composed-arbitrable/composed/MultiPartyInsurableArbitrableAgreementsBase.sol"; + + +/** + * @title ArbitrableTokenList + * This is a T2CL for tokens. Tokens can be submitted and cleared with a time out for challenging. + */ +contract ArbitrableTokenList is MultiPartyInsurableArbitrableAgreementsBase { + + /* Enums */ + + enum ItemStatus { + Absent, // The item has never been submitted. + Cleared, // The item has been submitted and the dispute resolution process determined it should not be added or a clearing request has been submitted and the dispute resolution process determined it should be cleared or the clearing was never contested. + Resubmitted, // The item has been cleared but someone has resubmitted it. + Registered, // The item has been submitted and the dispute resolution process determined it should be added or the submission was never contested. + Submitted, // The item has been submitted. + ClearingRequested, // The item is registered, but someone has requested to remove it. + PreventiveClearingRequested // The item has never been registered, but someone asked to clear it preemptively to avoid it being shown as not registered during the dispute resolution process. + } + + enum RulingOption { + OTHER, // Arbitrator did not rule of refused to rule. + EXECUTE, // Execute request. Rule in favor of requester. + REFUSE // Refuse request. Rule in favor of challenger. + } + + /* Structs */ + + struct Item { + ItemStatus status; // Status of the item. + uint lastAction; // Time of the last action. + uint balance; // The amount of funds placed at stake for this item. Does not include arbitrationFees. + uint challengeReward; // The challengeReward of the item for the round. + bytes32 latestAgreementID; // The ID of the latest agreement for the item. + } + + /* Modifiers */ + + modifier onlyT2CLGovernor {require(msg.sender == t2clGovernor, "The caller is not the t2cl governor."); _;} + + /* Events */ + + /** + * @dev Called when the item's status changes or when it is challenged/resolved. + * @param requester Address of the requester. + * @param challenger Address of the challenger, if any. + * @param tokenID The tokenID of the item. + * @param status The status of the item. + * @param disputed Wether the item is being disputed. + */ + event ItemStatusChange( + address indexed requester, + address indexed challenger, + bytes32 indexed tokenID, + ItemStatus status, + bool disputed + ); + + /* Storage */ + + // Settings + uint public challengeReward; // The stake deposit required in addition to arbitration fees for challenging a request. + uint public timeToChallenge; // The time before a request becomes executable if not challenged. + uint public arbitrationFeesWaitingTime; // The maximum time to wait for arbitration fees if the dispute is raised. + address public t2clGovernor; // The address that can update t2clGovernor, arbitrationFeesWaitingTime and challengeReward. + + // Items + mapping(bytes32 => Item) public items; + bytes32[] public itemsList; + + // Agreement and Item Extension + mapping(bytes32 => bytes32) public agreementIDToItemID; + + /* Constructor */ + + /** + * @dev Constructs the arbitrable token list. + * @param _arbitrator The chosen arbitrator. + * @param _arbitratorExtraData Extra data for the arbitrator contract. + * @param _feeGovernor The fee governor of this contract. + * @param _stake The stake parameter for sharing fees. + * @param _t2clGovernor The t2clGovernor address. This address can update t2clGovernor, arbitrationFeesWaitingTime and challengeReward. + * @param _arbitrationFeesWaitingTime The maximum time to wait for arbitration fees if the dispute is raised. + * @param _challengeReward The amount in Weis of deposit required for a submission or a challenge in addition to the arbitration fees. + * @param _timeToChallenge The time in seconds, parties have to challenge. + */ + constructor( + Arbitrator _arbitrator, + bytes _arbitratorExtraData, + address _feeGovernor, + uint _stake, + address _t2clGovernor, + uint _arbitrationFeesWaitingTime, + uint _challengeReward, + uint _timeToChallenge + ) public MultiPartyInsurableArbitrableAgreementsBase(_arbitrator, _arbitratorExtraData, _feeGovernor, _stake){ + challengeReward = _challengeReward; + timeToChallenge = _timeToChallenge; + t2clGovernor = _t2clGovernor; + arbitrationFeesWaitingTime = _arbitrationFeesWaitingTime; + } + + /* Public */ + + /** + * @dev Request for an item to be registered. + * @param _tokenID The keccak hash of a JSON object with all of the token's properties and no insignificant whitespaces. + * @param _metaEvidence The meta evidence for the potential dispute. + */ + function requestRegistration( + bytes32 _tokenID, + string _metaEvidence + ) external payable { + Item storage item = items[_tokenID]; + Agreement storage prevAgreement = agreements[latestAgreementID(_tokenID)]; + if(item.latestAgreementID != 0x0) // Not the first request for this tokenID. + require(prevAgreement.executed && !prevAgreement.disputed, "There is already a request in place."); + require(msg.value >= challengeReward, "Not enough ETH."); + + if (item.status == ItemStatus.Absent) + item.status = ItemStatus.Submitted; + else if (item.status == ItemStatus.Cleared) + item.status = ItemStatus.Resubmitted; + else + revert("Item in wrong status for registration."); // If the item is neither Absent nor Cleared, it is not possible to request registering it. + + if (item.lastAction == 0) { + itemsList.push(_tokenID); + } + + item.balance = challengeReward; + item.lastAction = now; + item.challengeReward = challengeReward; // Update challengeReward. + + address[] memory _parties = new address[](2); + _parties[0] = msg.sender; + + _createAgreement( + _metaEvidence, + _parties, + 2, + new bytes(0), + arbitrationFeesWaitingTime, + arbitrator, + _tokenID + ); + + if(msg.value > challengeReward) msg.sender.transfer(msg.value - challengeReward); // Refund any extra ETH. + Agreement storage agreement = agreements[latestAgreementID(_tokenID)]; + emit ItemStatusChange(agreement.parties[0], address(0), _tokenID, item.status, agreement.disputed); + } + + /** + * @dev Request an item to be cleared. + * @param _tokenID The keccak hash of a JSON object with all of the token's properties and no insignificant whitespaces. + * @param _metaEvidence The meta evidence for the potential dispute. + */ + function requestClearing( + bytes32 _tokenID, + string _metaEvidence + ) external payable { + Item storage item = items[_tokenID]; + Agreement storage prevAgreement = agreements[latestAgreementID(_tokenID)]; + if(item.latestAgreementID != 0x0) // Not the first request for this tokenID. + require(prevAgreement.executed && !prevAgreement.disputed, "There is already a request in place."); + require(msg.value >= challengeReward, "Not enough ETH."); + + if (item.status == ItemStatus.Registered) + item.status = ItemStatus.ClearingRequested; + else if (item.status == ItemStatus.Absent) + item.status = ItemStatus.PreventiveClearingRequested; + else + revert("Item in wrong status for clearing."); // If the item is neither Registered nor Absent, it is not possible to request clearing it. + + if (item.lastAction == 0) { + itemsList.push(_tokenID); + } + + item.balance = challengeReward; // Update challengeReward. + item.lastAction = now; + item.challengeReward = challengeReward; + + address[] memory _parties = new address[](2); + _parties[0] = msg.sender; + + _createAgreement( + _metaEvidence, + _parties, + 2, + new bytes(0), + arbitrationFeesWaitingTime, + arbitrator, + _tokenID + ); + + if(msg.value > challengeReward) msg.sender.transfer(msg.value - challengeReward); // Refund any extra eth. + Agreement storage agreement = agreements[latestAgreementID(_tokenID)]; + emit ItemStatusChange(agreement.parties[0], address(0), _tokenID, item.status, agreement.disputed); + } + + /** @dev Overrides parent to use information specific to Arbitrable Token List in math: + * - Parent's fundDispute doesn't take into account `challengeReward` when calculating ETH. + * - For calls that initiate a dispute, msg.value must also include `challengeReward`. + * @param _agreementID The ID of the agreement. + * @param _side The side. 0 for the side that lost the previous round, if any, and 1 for the one that won. + */ + function fundDispute(bytes32 _agreementID, uint _side) public payable { + Agreement storage agreement = agreements[_agreementID]; + PaidFees storage _paidFees = paidFees[_agreementID]; + Item storage item = items[agreementIDToItemID[_agreementID]]; + require(agreement.creator != address(0), "The specified agreement does not exist."); + require(!agreement.executed, "You cannot fund disputes for executed agreements."); + require( + !agreement.disputed || agreement.arbitrator.disputeStatus(agreement.disputeID) == Arbitrator.DisputeStatus.Appealable, + "The agreement is already disputed and is not appealable." + ); + require(_side <= 1, "There are only two sides."); + + // Prepare storage for first call. + if (_paidFees.firstContributionTime == 0) { + _paidFees.firstContributionTime = now; + _paidFees.ruling.push(0); + _paidFees.stake.push(stake); + _paidFees.totalValue.push(0); + _paidFees.totalContributedPerSide.push([0, 0]); + _paidFees.loserFullyFunded.push(false); + _paidFees.contributions.length++; + } else { // Reset cache. + fundDisputeCache.cost = 0; + fundDisputeCache.appealing = false; + (fundDisputeCache.appealPeriodStart, fundDisputeCache.appealPeriodEnd) = (0, 0); + fundDisputeCache.appealPeriodSupported = false; + fundDisputeCache.requiredValueForSide = 0; + fundDisputeCache.expectedValue = 0; + fundDisputeCache.stillRequiredValueForSide = 0; + fundDisputeCache.keptValue = 0; + fundDisputeCache.refundedValue = 0; + } + + // Check time outs and requirements. + if (_paidFees.stake.length == 1) { // First round. + fundDisputeCache.cost = agreement.arbitrator.arbitrationCost(agreement.extraData); + + // Arbitration fees time out. + if (now - _paidFees.firstContributionTime > agreement.arbitrationFeesWaitingTime) { + executeAgreementRuling(_agreementID, 0); + return; + } + } else { // Appeal. + fundDisputeCache.cost = agreement.arbitrator.appealCost(agreement.disputeID, agreement.extraData); + + fundDisputeCache.appealing = true; + (fundDisputeCache.appealPeriodStart, fundDisputeCache.appealPeriodEnd) = agreement.arbitrator.appealPeriod(agreement.disputeID); + fundDisputeCache.appealPeriodSupported = fundDisputeCache.appealPeriodStart != 0 && fundDisputeCache.appealPeriodEnd != 0; + if (fundDisputeCache.appealPeriodSupported) { + if (now < fundDisputeCache.appealPeriodStart + ((fundDisputeCache.appealPeriodEnd - fundDisputeCache.appealPeriodStart) / 2)) // In the first half of the appeal period. + require(_side == 0, "It is the losing side's turn to fund the appeal."); + else // In the second half of the appeal period. + require( + _side == 1 && _paidFees.loserFullyFunded[_paidFees.loserFullyFunded.length - 1], + "It is the winning side's turn to fund the appeal, only if the losing side already fully funded it." + ); + } else require(msg.value >= fundDisputeCache.cost, "Fees must be paid in full if the arbitrator does not support `appealPeriod`."); + } + require(msg.value > 0, "The value of the contribution cannot be zero."); + + // Compute required value. + if (!fundDisputeCache.appealing) { // First round. + fundDisputeCache.requiredValueForSide = fundDisputeCache.cost / 2; + } else { // Appeal. + if (!fundDisputeCache.appealPeriodSupported) + fundDisputeCache.requiredValueForSide = fundDisputeCache.cost; + else if (_side == 0) // Losing side. + fundDisputeCache.requiredValueForSide = fundDisputeCache.cost + (2 * _paidFees.stake[_paidFees.stake.length - 1]); + else { // Winning side. + fundDisputeCache.expectedValue = _paidFees.totalContributedPerSide[_paidFees.totalContributedPerSide.length - 1][0] - _paidFees.stake[_paidFees.stake.length - 1]; + fundDisputeCache.requiredValueForSide = fundDisputeCache.cost > _paidFees.totalContributedPerSide[_paidFees.totalContributedPerSide.length - 1][0] + fundDisputeCache.expectedValue ? fundDisputeCache.cost - _paidFees.totalContributedPerSide[_paidFees.totalContributedPerSide.length - 1][0] : fundDisputeCache.expectedValue; + } + } + + // Take contribution. + if (_paidFees.totalContributedPerSide[_paidFees.totalContributedPerSide.length - 1][_side] >= fundDisputeCache.requiredValueForSide) + fundDisputeCache.stillRequiredValueForSide = 0; + else + fundDisputeCache.stillRequiredValueForSide = fundDisputeCache.requiredValueForSide - _paidFees.totalContributedPerSide[_paidFees.totalContributedPerSide.length - 1][_side]; + + if(item.balance == item.challengeReward) { // Party is attempting to start a dispute. + require(msg.value >= item.challengeReward, "Party challenging agreement must place value at stake"); + fundDisputeCache.keptValue = fundDisputeCache.stillRequiredValueForSide >= msg.value - item.challengeReward + ? msg.value - item.challengeReward + : fundDisputeCache.stillRequiredValueForSide; + item.balance += item.challengeReward; + fundDisputeCache.refundedValue = msg.value - fundDisputeCache.keptValue - item.challengeReward; + agreement.parties[1] = msg.sender; + } else { // Party that started dispute already placed value at stake. + fundDisputeCache.keptValue = fundDisputeCache.stillRequiredValueForSide >= msg.value + ? msg.value + : fundDisputeCache.stillRequiredValueForSide; + fundDisputeCache.refundedValue = msg.value - fundDisputeCache.keptValue; + } + + if (fundDisputeCache.keptValue > 0) { + _paidFees.totalValue[_paidFees.totalValue.length - 1] += fundDisputeCache.keptValue; + _paidFees.totalContributedPerSide[_paidFees.totalContributedPerSide.length - 1][_side] += fundDisputeCache.keptValue; + _paidFees.contributions[_paidFees.contributions.length - 1][msg.sender][_side] += fundDisputeCache.keptValue; + } + if (fundDisputeCache.refundedValue > 0) msg.sender.transfer(fundDisputeCache.refundedValue); + emit Contribution(_agreementID, _paidFees.stake.length - 1, msg.sender, fundDisputeCache.keptValue); + + // Check if enough funds have been gathered and act accordingly. + if ( + _paidFees.totalContributedPerSide[_paidFees.totalContributedPerSide.length - 1][_side] >= fundDisputeCache.requiredValueForSide || + (fundDisputeCache.appealing && !fundDisputeCache.appealPeriodSupported) + ) { + if (_side == 0 && (fundDisputeCache.appealing ? fundDisputeCache.appealPeriodSupported : _paidFees.totalContributedPerSide[_paidFees.totalContributedPerSide.length - 1][1] < fundDisputeCache.requiredValueForSide)) { // Losing side and not direct appeal or dispute raise. + if (!_paidFees.loserFullyFunded[_paidFees.loserFullyFunded.length - 1]) + _paidFees.loserFullyFunded[_paidFees.loserFullyFunded.length - 1] = true; + } else { // Winning side or direct appeal. + if (!fundDisputeCache.appealing) { // First round. + if (_paidFees.totalContributedPerSide[_paidFees.totalContributedPerSide.length - 1][_side == 0 ? 1 : 0] < fundDisputeCache.requiredValueForSide) return; + agreement.disputeID = agreement.arbitrator.createDispute.value(fundDisputeCache.cost)(agreement.numberOfChoices, agreement.extraData); + agreement.disputed = true; + arbitratorAndDisputeIDToAgreementID[agreement.arbitrator][agreement.disputeID] = _agreementID; + emit Dispute(agreement.arbitrator, agreement.disputeID, uint(_agreementID)); + } else { // Appeal. + _paidFees.ruling[_paidFees.ruling.length - 1] = agreement.arbitrator.currentRuling(agreement.disputeID); + agreement.arbitrator.appeal.value(fundDisputeCache.cost)(agreement.disputeID, agreement.extraData); + if (!agreement.appealed) agreement.appealed = true; + } + + // Update the total value. + _paidFees.totalValue[_paidFees.totalValue.length - 1] -= fundDisputeCache.cost; + + // Prepare for the next round. + _paidFees.ruling.push(0); + _paidFees.stake.push(stake); + _paidFees.totalValue.push(0); + _paidFees.totalContributedPerSide.push([0, 0]); + _paidFees.loserFullyFunded.push(false); + _paidFees.contributions.length++; + } + } + } + + /** + * @dev Execute a request after the time for challenging it has passed. Can be called by anyone. + * @param _tokenID The tokenID of the item with the request to execute. + */ + function executeRequest(bytes32 _tokenID) external { + Item storage item = items[_tokenID]; + bytes32 agreementID = latestAgreementID(_tokenID); + Agreement storage agreement = agreements[agreementID]; + require(now - item.lastAction > timeToChallenge, "The time to challenge has not passed yet."); + require(agreement.creator != address(0), "The specified agreement does not exist."); + require(!agreement.executed, "The specified agreement has already been executed."); + require(!agreement.disputed, "The specified agreement is disputed."); + + if (item.status == ItemStatus.Resubmitted || item.status == ItemStatus.Submitted) + item.status = ItemStatus.Registered; + else if (item.status == ItemStatus.ClearingRequested || item.status == ItemStatus.PreventiveClearingRequested) + item.status = ItemStatus.Cleared; + else + revert("Item in wrong status for executing request."); + + agreement.parties[0].send(item.balance); // Deliberate use of send in order to not block the contract in case of reverting fallback. + agreement.executed = true; + item.lastAction = now; + item.challengeReward = 0; // Clear challengeReward once a request has been executed. + item.balance = 0; + + emit ItemStatusChange(agreement.parties[0], address(0), _tokenID, item.status, agreement.disputed); + } + + /** @dev Changes the `timeToChallenge` storage variable. + * @param _timeToChallenge The new `timeToChallenge` storage variable. + */ + function changeTimeToChallenge(uint _timeToChallenge) external onlyT2CLGovernor { + timeToChallenge = _timeToChallenge; + } + + /** @dev Changes the `challengeReward` storage variable. + * @param _challengeReward The new `challengeReward` storage variable. + */ + function changeChallengeReward(uint _challengeReward) external onlyT2CLGovernor { + challengeReward = _challengeReward; + } + + /** @dev Changes the `t2clGovernor` storage variable. + * @param _t2clGovernor The new `t2clGovernor` storage variable. + */ + function changeT2CLGovernor(address _t2clGovernor) external onlyT2CLGovernor { + t2clGovernor = _t2clGovernor; + } + + /** @dev Changes the `arbitrationFeesWaitingTime` storage variable. + * @param _arbitrationFeesWaitingTime The new `_arbitrationFeesWaitingTime` storage variable. + */ + function changeArbitrationFeesWaitingTime(uint _arbitrationFeesWaitingTime) external onlyT2CLGovernor { + arbitrationFeesWaitingTime = _arbitrationFeesWaitingTime; + } + + /* Public Views */ + + /** + * @dev Returns the latest agreement for an item. + * @param _tokenID The tokenID of the item to check. + * @return The latest agreementID. + */ + function latestAgreementID(bytes32 _tokenID) public view returns (bytes32) { + return items[_tokenID].latestAgreementID; + } + + /** + * @dev Return true if the item is allowed. + * We consider the item to be in the list if its status is contested and it has not won a dispute previously. + * @param _tokenID The tokenID of the item to check. + * @return allowed True if the item is allowed, false otherwise. + */ + function isPermitted(bytes32 _tokenID) public view returns (bool allowed) { + Item storage item = items[_tokenID]; + return item.status == ItemStatus.Registered || item.status == ItemStatus.ClearingRequested; + } + + /* Internal */ + + /** @dev Extends parent to use counter identify agreements. + * @param _metaEvidence The meta evidence of the agreement. + * @param _parties The `parties` value of the agreement. + * @param _numberOfChoices The `numberOfChoices` value of the agreement. + * @param _extraData The `extraData` value of the agreement. + * @param _arbitrationFeesWaitingTime The `arbitrationFeesWaitingTime` value of the agreement. + * @param _arbitrator The `arbitrator` value of the agreement. + * @param _tokenID The item id. + */ + function _createAgreement( + string _metaEvidence, + address[] _parties, + uint _numberOfChoices, + bytes _extraData, + uint _arbitrationFeesWaitingTime, + Arbitrator _arbitrator, + bytes32 _tokenID + ) internal { + Item storage item = items[_tokenID]; + bytes32 agreementID; + if(item.latestAgreementID == 0x0) + agreementID = keccak256(abi.encodePacked(_tokenID)); + else + agreementID = keccak256(abi.encodePacked(item.latestAgreementID)); + + item.latestAgreementID = agreementID; + agreementIDToItemID[agreementID] = _tokenID; + + super._createAgreement( + agreementID, + _metaEvidence, + _parties, + _numberOfChoices, + _extraData, + _arbitrationFeesWaitingTime, + _arbitrator + ); + } + + /** @dev Executes the ruling on the specified agreement. + * @param _agreementID The ID of the agreement. + * @param _ruling The ruling. + */ + function executeAgreementRuling(bytes32 _agreementID, uint _ruling) internal { + super.executeAgreementRuling(_agreementID, _ruling); + require(_ruling <= 2, "Ruling must be valid"); + Agreement storage agreement = agreements[_agreementID]; + PaidFees storage _paidFees = paidFees[_agreementID]; + Item storage item = items[agreementIDToItemID[_agreementID]]; + + if (_paidFees.stake.length == 1) { // Failed to fund first round. + // Rule in favor of whoever paid more. + if (_paidFees.totalContributedPerSide[0][0] >= _paidFees.totalContributedPerSide[0][1]){ + // Ruled in favor of the requester. + if(item.status == ItemStatus.Submitted || item.status == ItemStatus.Resubmitted) + item.status = ItemStatus.Registered; + else + item.status = ItemStatus.Cleared; + + agreement.parties[0].send(item.balance); // Deliberate use of send in order to not block the contract in case of reverting fallback. + } else { + // Ruled in favor of the challenger. + if(item.status == ItemStatus.Resubmitted) + item.status = ItemStatus.Cleared; + else if (item.status == ItemStatus.ClearingRequested) + item.status = ItemStatus.Registered; + else + item.status = ItemStatus.Absent; + + agreement.parties[1].send(item.balance); // Deliberate use of send in order to not block the contract in case of reverting fallback. + } + } else { + // Respect the ruling unless the losing side funded the appeal and the winning side paid less than expected + // and the arbitrator did not refuse to rule. + if ( + _paidFees.loserFullyFunded[_paidFees.loserFullyFunded.length - 1] && + _paidFees.totalContributedPerSide[_paidFees.totalContributedPerSide.length - 1][0] - _paidFees.stake[_paidFees.stake.length - 1] > _paidFees.totalContributedPerSide[_paidFees.totalContributedPerSide.length - 1][1] && + _ruling != uint(RulingOption.OTHER) // Respect the ruling if the arbitrator refused to rule. + ) { + // Rule in favor of the loosing party. + // Ruling in favor of the loosing party here means doing the opposite of the decision of the arbitrator. + // This is the case because not enough funds were raised to raise a new dispute. + if (_ruling == uint(RulingOption.EXECUTE)) { + // Revert to previous state. + if (item.status == ItemStatus.Resubmitted) + item.status = ItemStatus.Cleared; + else if (item.status == ItemStatus.ClearingRequested) + item.status = ItemStatus.Registered; + else + item.status = ItemStatus.Absent; + + // Reward challenger. + agreement.parties[1].send(item.balance); // Deliberate use of send in order to not block the contract in case of reverting fallback. + } else { + // Execute request. + if (item.status == ItemStatus.Submitted || item.status == ItemStatus.Resubmitted) + item.status = ItemStatus.Registered; + else + item.status = ItemStatus.Cleared; + + // Reward requester. + agreement.parties[0].send(item.balance); // Deliberate use of send in order to not block the contract in case of reverting fallback. + } + } else { + // Respect the ruling. + if (_ruling == uint(RulingOption.EXECUTE)) { + // Execute the request. + if (item.status == ItemStatus.Resubmitted || item.status == ItemStatus.Submitted) + item.status = ItemStatus.Registered; + else + item.status = ItemStatus.Cleared; + + // Send rewards to requester. + agreement.parties[0].send(item.balance); // Deliberate use of send in order to not block the contract in case of reverting fallback. + } else if (_ruling == uint(RulingOption.REFUSE)) { + if (item.status == ItemStatus.Resubmitted || item.status == ItemStatus.Submitted) + item.status = ItemStatus.Cleared; + else + item.status = ItemStatus.Registered; + + // Send rewards to challenger. + agreement.parties[1].send(item.balance); // Deliberate use of send in order to not block the contract in case of reverting fallback. + } else { + // Arbitrator refused to rule. + // Revert to previous state. + if (item.status == ItemStatus.Resubmitted) + item.status = ItemStatus.Cleared; + else if (item.status == ItemStatus.ClearingRequested) + item.status = ItemStatus.Registered; + else + item.status = ItemStatus.Absent; + + // Split the balance 50-50 and give the item the initial status. + agreement.parties[0].send(item.balance / 2); // Deliberate use of send in order to not block the contract in case of reverting fallback. + agreement.parties[1].send(item.balance / 2); // Deliberate use of send in order to not block the contract in case of reverting fallback. + } + } + + } + + agreement.disputed = false; + agreement.executed = true; + item.lastAction = now; + item.balance = 0; + item.challengeReward = 0; // Clear challengeReward once a dispute is resolved. + + emit ItemStatusChange(agreement.parties[0], address(0), agreementIDToItemID[_agreementID], item.status, agreement.disputed); + } + + /* Interface Views */ + + /** + * @dev Return the numbers of items in the list per status. + * @return The numbers of items in the list per status. + */ + function itemsCounts() + external + view + returns ( + uint disputed, + uint absent, + uint cleared, + uint resubmitted, + uint submitted, + uint clearingRequested, + uint preventiveClearingRequested + ) + { + for (uint i = 0; i < itemsList.length; i++) { + Item storage item = items[itemsList[i]]; + Agreement storage latestAgreement = agreements[latestAgreementID(itemsList[i])]; + + if (latestAgreement.disputed) disputed++; + else if (item.status == ItemStatus.Absent) absent++; + else if (item.status == ItemStatus.Cleared) cleared++; + else if (item.status == ItemStatus.Submitted) submitted++; + else if (item.status == ItemStatus.Resubmitted) resubmitted++; + else if (item.status == ItemStatus.ClearingRequested) clearingRequested++; + else if (item.status == ItemStatus.PreventiveClearingRequested) preventiveClearingRequested++; + } + } + + /** + * @dev Return the values of the items the query finds. + * This function is O(n) at worst, where n is the number of items. This could exceed the gas limit, therefore this function should only be used for interface display and not by other contracts. + * @param _cursor The pagination cursor. + * @param _count The number of items to return. + * @param _filter The filter to use. + * @param _sort The sort order to use. + * @return The values of the items found and wether there are more items for the current filter and sort. + */ + function queryItems(bytes32 _cursor, uint _count, bool[9] _filter, bool _sort) external view returns (bytes32[] values, bool hasMore) { + uint _cursorIndex; + values = new bytes32[](_count); + uint _index = 0; + + if (_cursor == 0) + _cursorIndex = 0; + else { + for (uint j = 0; j < itemsList.length; j++) { + if (itemsList[j] == _cursor) { + _cursorIndex = j; + break; + } + } + require(_cursorIndex != 0, "The cursor is invalid."); + } + + for ( + uint i = _cursorIndex == 0 ? (_sort ? 0 : 1) : (_sort ? _cursorIndex + 1 : itemsList.length - _cursorIndex + 1); + _sort ? i < itemsList.length : i <= itemsList.length; + i++ + ) { // Oldest or newest first. + bytes32 itemID = itemsList[_sort ? i : itemsList.length - i]; + Item storage item = items[itemID]; + Agreement storage agreement = agreements[item.latestAgreementID]; + if ( + (_filter[0] && agreement.disputed) || + (_filter[1] && item.status == ItemStatus.Absent) || + (_filter[2] && item.status == ItemStatus.Cleared) || + (_filter[3] && item.status == ItemStatus.Submitted) || + (_filter[4] && item.status == ItemStatus.Resubmitted) || + (_filter[5] && item.status == ItemStatus.ClearingRequested) || + (_filter[6] && item.status == ItemStatus.PreventiveClearingRequested) || + (_filter[7] && agreement.parties[0] == msg.sender) || // My Submissions. + (_filter[8] && agreement.parties[1] == msg.sender) // My Challenges. + ) { + if (_index < _count) { + values[_index] = itemsList[_sort ? i : itemsList.length - i]; + _index++; + } else { + hasMore = true; + break; + } + } + } + } + +} \ No newline at end of file diff --git a/test/arbitrable-token-list.js b/test/arbitrable-token-list.js new file mode 100644 index 00000000..f90e1632 --- /dev/null +++ b/test/arbitrable-token-list.js @@ -0,0 +1,2640 @@ +/* eslint-disable no-undef */ // Avoid the linter considering truffle elements as undef. + +const BigNumber = web3.BigNumber +const { + expectThrow +} = require('openzeppelin-solidity/test/helpers/expectThrow') +const { + increaseTime +} = require('openzeppelin-solidity/test/helpers/increaseTime') + +const ArbitrableTokenList = artifacts.require('./ArbitrableTokenList.sol') +const AppealableArbitrator = artifacts.require( + './standard/arbitration/AppealableArbitrator.sol' +) +const EnhancedAppealableArbitrator = artifacts.require( + './standard/arbitration/EnhancedAppealableArbitrator.sol' +) + +contract('ArbitrableTokenList', function(accounts) { + const governor = accounts[0] + const partyA = accounts[2] + const partyB = accounts[8] + const t2clGovernor = accounts[9] + const arbitratorExtraData = 0x08575 + const challengeReward = 10 ** 10 + const arbitrationPrice = 100 + const halfOfArbitrationPrice = arbitrationPrice / 2 + const timeToChallenge = 0 + const metaEvidence = 'evidence' + const feeStake = 10 + const arbitrationFeesWaitingTime = 1000 + const appealPeriodDuration = 1000 + + let appealableArbitrator + let enhancedAppealableArbitrator + let arbitrableTokenList + + const ITEM_STATUS = { + ABSENT: 0, + CLEARED: 1, + RESUBMITTED: 2, + REGISTERED: 3, + SUBMITTED: 4, + CLEARING_REQUESTED: 5, + PREVENTIVE_CLEARING_REQUESTED: 6 + } + + const RULING = { OTHER: 0, EXECUTE: 1, REFUSE: 2 } + const TOKEN_ID = 'pnk' + + const deployArbitrators = async () => { + appealableArbitrator = await AppealableArbitrator.new( + arbitrationPrice, // _arbitrationPrice + governor, // _arbitrator + null, // _arbitratorExtraData + appealPeriodDuration // _appealPeriodDuration + ) + await appealableArbitrator.changeArbitrator(appealableArbitrator.address) + + enhancedAppealableArbitrator = await EnhancedAppealableArbitrator.new( + arbitrationPrice, // _arbitrationPrice + governor, // _arbitrator + null, // _arbitratorExtraData + appealPeriodDuration // _timeOut + ) + await enhancedAppealableArbitrator.changeArbitrator( + enhancedAppealableArbitrator.address + ) + } + + const deployArbitrableTokenList = async arbitrator => { + arbitrableTokenList = await ArbitrableTokenList.new( + arbitrator.address, // arbitrator + arbitratorExtraData, + arbitrator.address, // fee governor + feeStake, + t2clGovernor, + arbitrationFeesWaitingTime, + challengeReward, + timeToChallenge + ) + } + + describe('queryItems', () => { + before('setup contract for each test', async () => { + await deployArbitrators() + await deployArbitrableTokenList(appealableArbitrator) + }) + + before('populate the list', async () => { + await arbitrableTokenList.requestRegistration(TOKEN_ID, metaEvidence, { + from: partyA, + value: challengeReward + }) + }) + + it('should succesfully retrieve mySubmissions', async function() { + const cursor = 0 + const count = 1 + + const disputed = false + const absent = false + const cleared = false + const submitted = false + const resubmitted = false + const clearingRequested = false + const preventiveClearingRequested = false + const mySubmissions = true + const myChallenges = false + + const filter = [ + disputed, + absent, + cleared, + submitted, + resubmitted, + clearingRequested, + preventiveClearingRequested, + mySubmissions, + myChallenges + ] + const sort = true + const item = (await arbitrableTokenList.queryItems( + cursor, + count, + filter, + sort, + { from: partyA } + ))[0] + + assert.equal(web3.toUtf8(item[0]), TOKEN_ID) + }) + + it('should succesfully retrieve submitted', async function() { + const cursor = 0 + const count = 1 + + const disputed = false + const absent = false + const cleared = false + const submitted = true + const resubmitted = false + const clearingRequested = false + const preventiveClearingRequested = false + const mySubmissions = false + const myChallenges = false + + const filter = [ + disputed, + absent, + cleared, + submitted, + resubmitted, + clearingRequested, + preventiveClearingRequested, + mySubmissions, + myChallenges + ] + const sort = true + const item = (await arbitrableTokenList.queryItems( + cursor, + count, + filter, + sort, + { from: partyA } + ))[0] + + assert.equal(web3.toUtf8(item[0]), TOKEN_ID) + }) + + it('should revert when not cursor < itemsList.length', async function() { + const cursor = 1 + const count = 1 + + const disputed = false + const absent = false + const cleared = false + const submitted = true + const resubmitted = false + const clearingRequested = false + const preventiveClearingRequested = false + const mySubmissions = false + const myChallenges = false + + const filter = [ + disputed, + absent, + cleared, + submitted, + resubmitted, + clearingRequested, + preventiveClearingRequested, + mySubmissions, + myChallenges + ] + const sort = true + + await expectThrow( + arbitrableTokenList.queryItems(cursor, count, filter, sort, { + from: partyA + }) + ) + }) + }) + + describe('governance', async () => { + beforeEach(async () => { + await deployArbitrators() + await deployArbitrableTokenList(appealableArbitrator) + }) + + describe('caller is t2clGovernor', () => { + beforeEach(async () => { + assert.notEqual( + partyB, + t2clGovernor, + 'partyB and t2clGovernor should be different for this test' + ) + }) + + it('should update t2clGovernor', async () => { + const challengeRewardBefore = await arbitrableTokenList.t2clGovernor() + await arbitrableTokenList.changeT2CLGovernor(partyB, { + from: t2clGovernor + }) + + const challengeRewardAfter = await arbitrableTokenList.t2clGovernor() + assert.notEqual( + challengeRewardAfter, + challengeRewardBefore, + 't2clGovernor should have changed' + ) + assert.equal( + challengeRewardAfter, + partyB, + 't2clGovernor should be partyB' + ) + }) + + it('should update challangeReward', async () => { + const challengeRewardBefore = await arbitrableTokenList.challengeReward() + const newChallengeReward = challengeRewardBefore.toNumber() + 1000 + + await arbitrableTokenList.changeChallengeReward(newChallengeReward, { + from: t2clGovernor + }) + + const challengeRewardAfter = await arbitrableTokenList.challengeReward() + assert.notEqual( + challengeRewardAfter, + challengeRewardBefore, + 'challengeReward should have changed' + ) + assert.equal( + challengeRewardAfter.toNumber(), + newChallengeReward, + 'challengeReward should have changed' + ) + }) + + it('should update arbitrationFeesWaitingTime', async () => { + const arbitrationFeesWaitingTimeBefore = await arbitrableTokenList.arbitrationFeesWaitingTime() + const newarbitrationFeesWaitingTime = + arbitrationFeesWaitingTimeBefore.toNumber() + 1000 + + await arbitrableTokenList.changeArbitrationFeesWaitingTime( + newarbitrationFeesWaitingTime, + { + from: t2clGovernor + } + ) + + const arbitrationFeesWaitingTimeAfter = await arbitrableTokenList.arbitrationFeesWaitingTime() + assert.notEqual( + arbitrationFeesWaitingTimeAfter, + arbitrationFeesWaitingTimeBefore, + 'arbitrationFeesWaitingTime should have changed' + ) + assert.equal( + arbitrationFeesWaitingTimeAfter.toNumber(), + newarbitrationFeesWaitingTime, + 'arbitrationFeesWaitingTime should have changed' + ) + }) + + it('should update timeToChallenge', async () => { + const timeToChallengeBefore = await arbitrableTokenList.timeToChallenge() + const newTimeToChallenge = timeToChallengeBefore.toNumber() + 1000 + + await arbitrableTokenList.changeTimeToChallenge(newTimeToChallenge, { + from: t2clGovernor + }) + + const timeToChallengeAfter = await arbitrableTokenList.timeToChallenge() + assert.notEqual( + timeToChallengeAfter.toNumber(), + timeToChallengeBefore.toNumber(), + 'timeToChallenge should have changed' + ) + assert.equal( + timeToChallengeAfter.toNumber(), + newTimeToChallenge, + 'timeToChallenge should have changed' + ) + }) + }) + + describe('caller is not t2clGovernor', () => { + beforeEach(async () => { + assert.notEqual( + partyA, + t2clGovernor, + 'partyA and t2clGovernor should be different for this test' + ) + }) + + it('should not update t2clGovernor', async () => { + const t2clGovernorBefore = await arbitrableTokenList.t2clGovernor() + await expectThrow( + arbitrableTokenList.changeT2CLGovernor(partyB, { + from: partyB + }) + ) + + const t2clGovernorAfter = await arbitrableTokenList.t2clGovernor() + assert.equal( + t2clGovernorAfter, + t2clGovernorBefore, + 't2clGovernor should not have changed' + ) + assert.notEqual( + t2clGovernorAfter, + partyB, + 't2clGovernor should not be partyB' + ) + }) + + it('should not update challangeReward', async () => { + const challengeRewardBefore = await arbitrableTokenList.challengeReward() + const newChallengeReward = challengeRewardBefore.toNumber() + 1000 + + await expectThrow( + arbitrableTokenList.changeChallengeReward(newChallengeReward, { + from: partyB + }) + ) + + const challengeRewardAfter = await arbitrableTokenList.challengeReward() + assert.equal( + challengeRewardAfter.toNumber(), + challengeRewardBefore.toNumber(), + 'challengeReward should not have changed' + ) + assert.notEqual( + challengeRewardAfter.toNumber(), + newChallengeReward, + 'challengeReward should not have changed' + ) + }) + + it('should not update arbitrationFeesWaitingTime', async () => { + const arbitrationFeesWaitingTimeBefore = await arbitrableTokenList.arbitrationFeesWaitingTime() + const newArbitrationFeesWaitingTime = + arbitrationFeesWaitingTimeBefore.toNumber() + 1000 + + await expectThrow( + arbitrableTokenList.changeArbitrationFeesWaitingTime( + newArbitrationFeesWaitingTime, + { + from: partyB + } + ) + ) + + const arbitrationFeesWaitingTimeAfter = await arbitrableTokenList.arbitrationFeesWaitingTime() + assert.equal( + arbitrationFeesWaitingTimeAfter.toNumber(), + arbitrationFeesWaitingTimeBefore.toNumber(), + 'arbitrationFeesWaitingTime should not have changed' + ) + assert.notEqual( + arbitrationFeesWaitingTimeAfter.toNumber(), + newArbitrationFeesWaitingTime, + 'arbitrationFeesWaitingTime should not have changed' + ) + }) + + it('should not update timeToChallenge', async () => { + const timeToChallengeBefore = await arbitrableTokenList.timeToChallenge() + const newTimeToChallenge = timeToChallengeBefore.toNumber() + 1000 + + await expectThrow( + arbitrableTokenList.changeTimeToChallenge(newTimeToChallenge, { + from: partyA + }) + ) + + const timeToChallengeAfter = await arbitrableTokenList.timeToChallenge() + assert.equal( + timeToChallengeAfter.toNumber(), + timeToChallengeBefore.toNumber(), + 'timeToChallenge should not have changed' + ) + assert.notEqual( + timeToChallengeAfter.toNumber(), + newTimeToChallenge, + 'timeToChallenge should not have changed' + ) + }) + }) + }) + + describe('appeal period disabled', () => { + beforeEach(async () => { + await deployArbitrators() + }) + + describe('requestRegistration', () => { + beforeEach(async () => { + await deployArbitrableTokenList(appealableArbitrator) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + 0, + 'initial contract balance should be zero for this test' + ) + + let item = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + item[3].toNumber(), + 0, + 'item.challengeReward should have be 0 initially' + ) + + await arbitrableTokenList.requestRegistration(TOKEN_ID, metaEvidence, { + from: partyA, + value: challengeReward + }) + item = await arbitrableTokenList.items(TOKEN_ID) + + if (challengeReward > 0) + assert.equal( + item[3].toNumber(), + challengeReward, + 'item.challengeReward should === challengeReward' + ) + + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + challengeReward, + 'contract should have challengeReward' + ) + }) + + it('should decrease contract balance', async () => { + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID, { from: partyA }) + + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + 0, + 'contract should have returned the reward to the submitter' + ) + }) + + it('should change item and agreement state for each submission phase', async () => { + const firstAgreementId = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + const agreementBefore = await arbitrableTokenList.getAgreementInfo( + firstAgreementId + ) + + assert.equal(agreementBefore[0], partyA, 'partyA should be the creator') + assert.equal( + agreementBefore[6].toNumber(), + 0, + 'there should be no disputes' + ) + assert.equal(agreementBefore[7], false, 'there should be no disputes') + assert.equal( + agreementBefore[9].toNumber(), + 0, + 'there should be no ruling' + ) + assert.equal( + agreementBefore[10], + false, + 'request should not have executed yet' + ) + + const itemBefore = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + itemBefore[0].toNumber(), + ITEM_STATUS.SUBMITTED, + 'item should be in submitted state' + ) + assert.isAbove( + itemBefore[1].toNumber(), + 0, + 'time of last action should be above zero' + ) + assert.equal( + itemBefore[2].toNumber(), + challengeReward, + 'item balance should be equal challengeReward' + ) + + await increaseTime(1) // Increase time to test item.lastAction + await arbitrableTokenList.executeRequest(TOKEN_ID) + const agreementAfter = await arbitrableTokenList.getAgreementInfo( + firstAgreementId + ) + assert.equal(agreementAfter[0], partyA, 'partyA should be the creator') + assert.equal( + agreementAfter[6].toNumber(), + 0, + 'there should be no disputes' + ) + assert.equal(agreementAfter[7], false, 'there should be no disputes') + assert.equal( + agreementAfter[9].toNumber(), + 0, + 'there should be no ruling' + ) + assert.equal(agreementAfter[10], true, 'request should have executed') + + const itemAfter = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + itemAfter[0].toNumber(), + ITEM_STATUS.REGISTERED, + 'item should be in registered state' + ) + assert.isAbove( + itemAfter[1].toNumber(), + itemBefore[1].toNumber(), + 'time of last action should be after previous' + ) + assert.equal( + itemAfter[2].toNumber(), + 0, + 'challengeRewards should have been sent back to submitter' + ) + + await expectThrow( + // should not allow calling executeRequestAgain + arbitrableTokenList.executeRequest(TOKEN_ID) + ) + }) + + describe('both sides fully fund, dispute is raised', () => { + beforeEach(async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + from: partyB, + value: halfOfArbitrationPrice + challengeReward + }) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + challengeReward * 2 + halfOfArbitrationPrice, + 'contract shoulld have challengeReward * 2 + halfOfArbitrationPrice' + ) + await arbitrableTokenList.fundDispute(agreementID, 0, { + from: partyA, + value: halfOfArbitrationPrice + }) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + challengeReward * 2, + 'contract should only hold challengeReward * 2' + ) + const agreementBefore = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + assert.equal( + agreementBefore[1][0], + partyA, + 'side 0 should be party A' + ) + assert.equal( + agreementBefore[1][1], + partyB, + 'side 1 should be party B' + ) + assert.isTrue(agreementBefore[7], 'agreement should be disputed') + assert.isFalse(agreementBefore[8], 'agreement should not be appealed') + assert.isFalse( + agreementBefore[10], + 'agreement should have not been executed yet' + ) + const itemBefore = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + itemBefore[0].toNumber(), + ITEM_STATUS.SUBMITTED, + 'item should be submitted' + ) + assert.equal( + itemBefore[2].toNumber(), + challengeReward * 2, + 'item balance should hold funds from party A and B' + ) + await expectThrow(arbitrableTokenList.executeRequest(TOKEN_ID)) // should fail since item is disputed + }) + + describe('arbitrator rules in favor of partyA', () => { + beforeEach(async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + const agreementBefore = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + + // Rule in favor of partyA + await appealableArbitrator.giveRuling( + agreementBefore[6], + RULING.EXECUTE + ) + }) + + it('no appeals, item should be registered', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + const agreementBefore = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + const partyABalanceBefore = (await web3.eth.getBalance( + partyA + )).toNumber() + const partyBBalanceBefore = (await web3.eth.getBalance( + partyB + )).toNumber() + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling( + agreementBefore[6], + RULING.EXECUTE + ) + + agreement = await arbitrableTokenList.getAgreementInfo(agreementID) + assert.equal( + (await web3.eth.getBalance( + arbitrableTokenList.address + )).toNumber(), + 0, + 'contract should hold no balance' + ) + const agreementAfter = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + assert.equal( + agreementAfter[1][0], + partyA, + 'side 0 should still be party A' + ) + assert.equal( + agreementAfter[1][1], + partyB, + 'side 1 should still be party B' + ) + assert.isFalse( + agreementAfter[7], + 'agreement should no be disputed anymore' + ) + assert.isFalse( + agreementAfter[8], + 'agreement should not be appealed' + ) + assert.isTrue( + agreementAfter[10], + 'agreement should have been executed' + ) + + const itemAfter = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + itemAfter[0].toNumber(), + ITEM_STATUS.REGISTERED, + 'item should be registered' + ) + assert.equal( + itemAfter[2].toNumber(), + 0, + 'item balance should be empty' + ) + + const partyABalanceAfter = (await web3.eth.getBalance( + partyA + )).toNumber() + const partyBBalanceAfter = (await web3.eth.getBalance( + partyB + )).toNumber() + + assert.isAtMost( + partyBBalanceAfter, + partyBBalanceBefore, + 'partyB should have not been rewarded' + ) + assert.isAbove( + partyABalanceAfter, + partyABalanceBefore, + 'partyA should have been rewarded' + ) + assert.equal( + (await web3.eth.getBalance( + arbitrableTokenList.address + )).toNumber(), + 0, + 'contract funds should be 0' + ) + }) + }) + + describe('arbitrator rules in favor of partyB', () => { + beforeEach(async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + const agreementBefore = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + + // Rule in favor of partyB + await appealableArbitrator.giveRuling( + agreementBefore[6], + RULING.REFUSE + ) + }) + + describe('no appeals', () => { + it('should send funds to partyB', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + const agreementBefore = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + const partyABalanceBefore = (await web3.eth.getBalance( + partyA + )).toNumber() + const partyBBalanceBefore = (await web3.eth.getBalance( + partyB + )).toNumber() + + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling( + agreementBefore[6], + RULING.REFUSE + ) + + agreement = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + assert.equal( + (await web3.eth.getBalance( + arbitrableTokenList.address + )).toNumber(), + 0, + 'contract should hold no balance' + ) + const agreementAfter = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + assert.equal( + agreementAfter[1][0], + partyA, + 'side 0 should be party A' + ) + + const partyABalanceAfter = (await web3.eth.getBalance( + partyA + )).toNumber() + const partyBBalanceAfter = (await web3.eth.getBalance( + partyB + )).toNumber() + + assert.isAbove( + partyBBalanceAfter, + partyBBalanceBefore, + 'partyB should have been rewarded' + ) + assert.isAtMost( + partyABalanceAfter, + partyABalanceBefore, + 'partyA should have not been rewarded' + ) + assert.equal( + (await web3.eth.getBalance( + arbitrableTokenList.address + )).toNumber(), + 0, + 'contract funds should be 0' + ) + }) + + it('should clear item, execute and resolve dispute', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + const agreementBefore = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling( + agreementBefore[6], + RULING.REFUSE + ) + + agreement = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + const agreementAfter = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + assert.equal( + agreementAfter[1][0], + partyA, + 'side 0 should still be party A' + ) + assert.equal( + agreementAfter[1][1], + partyB, + 'side 1 should still be party B' + ) + assert.isFalse( + agreementAfter[7], + 'agreement should no be disputed anymore' + ) + assert.isFalse( + agreementAfter[8], + 'agreement should not be appealed' + ) + assert.isTrue( + agreementAfter[10], + 'agreement should have been executed' + ) + + const itemAfter = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + itemAfter[0].toNumber(), + ITEM_STATUS.CLEARED, + 'item should be cleared' + ) + }) + }) + + describe('partyA appeals', () => { + describe('arbitrator rules in favor of partyB', () => {}) + }) + }) + }) + + describe('sides fails to fully fund', () => { + beforeEach(async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + from: partyB, + value: halfOfArbitrationPrice + challengeReward - 2 + }) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + challengeReward * 2 + halfOfArbitrationPrice - 2, + 'contract should have challengeReward * 2 + halfOfArbitrationPrice - 2' + ) + }) + + it('should register and reward submitter if he funds more', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + const itemBefore = await arbitrableTokenList.items(TOKEN_ID) + await arbitrableTokenList.fundDispute(agreementID, 0, { + from: partyA, + value: halfOfArbitrationPrice - 1 + }) + + const submitterBalanceBefore = await web3.eth.getBalance(partyA) + + await increaseTime(arbitrationFeesWaitingTime + 1) + const gasPrice = 100000000 + + const tx = await arbitrableTokenList.fundDispute(agreementID, 0, { + from: partyA, + gasPrice + }) + const consumed = tx.receipt.gasUsed * gasPrice + const submitterBalanceAfter = await web3.eth.getBalance(partyA) + + const itemAfter = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + submitterBalanceAfter.toNumber(), + submitterBalanceBefore.plus(challengeReward * 2).minus(consumed), + 'submitter should have received challengeReward * 2' + ) + + const agreementAfter = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + assert.equal( + itemAfter[0], + ITEM_STATUS.REGISTERED, + 'item should be registered' + ) + assert.isAbove( + itemAfter[1].toNumber(), + itemBefore[1].toNumber(), + 'last action should have increased' + ) + assert.equal( + itemAfter[2].toNumber(), + 0, + 'item balance should be zero' + ) + assert.equal( + agreementAfter[1][0], + partyA, + 'side 0 should still be party A' + ) + assert.equal( + agreementAfter[1][1], + partyB, + 'side 1 should still be party B' + ) + assert.isFalse(agreementAfter[7], 'agreement should not be disputed') + assert.isFalse(agreementAfter[8], 'agreement should not be appealed') + assert.isTrue( + agreementAfter[10], + 'agreement should have been executed' + ) + }) + + it('should clear item and reward challenger if he funds more', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + const itemBefore = await arbitrableTokenList.items(TOKEN_ID) + await arbitrableTokenList.fundDispute(agreementID, 0, { + from: partyA, + value: halfOfArbitrationPrice - 3 + }) + + const challengerBalanceBefore = await web3.eth.getBalance(partyB) + + await increaseTime(arbitrationFeesWaitingTime + 1) + const gasPrice = 100000000 + const tx = await arbitrableTokenList.fundDispute(agreementID, 0, { + from: partyB, + gasPrice + }) + const consumed = tx.receipt.gasUsed * gasPrice + challengerBalanceAfter = await web3.eth.getBalance(partyB) + + assert.equal( + challengerBalanceAfter.toNumber(), + challengerBalanceBefore + .plus(challengeReward * 2) + .minus(consumed) + .toNumber(), + 'challenger should have received challengeReward * 2' + ) + const itemAfter = await arbitrableTokenList.items(TOKEN_ID) + const agreementAfter = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + assert.equal( + itemAfter[0], + ITEM_STATUS.ABSENT, + 'item should be absent' + ) + assert.isAbove( + itemAfter[1].toNumber(), + itemBefore[1].toNumber(), + 'last action should have increased' + ) + assert.equal( + itemAfter[2].toNumber(), + 0, + 'item balance should be zero' + ) + assert.equal( + agreementAfter[1][0], + partyA, + 'side 0 should still be party A' + ) + assert.equal( + agreementAfter[1][1], + partyB, + 'side 1 should still be party B' + ) + assert.isFalse(agreementAfter[7], 'agreement should not be disputed') + assert.isFalse(agreementAfter[8], 'agreement should not be appealed') + assert.isTrue( + agreementAfter[10], + 'agreement should have been executed' + ) + }) + }) + }) + + describe('requestClearing', () => { + beforeEach(async () => { + await deployArbitrableTokenList(appealableArbitrator) + await arbitrableTokenList.requestRegistration(TOKEN_ID, metaEvidence, { + from: partyA, + value: challengeReward + }) + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID) + const firstAgreementId = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + const agreementSetup = await arbitrableTokenList.getAgreementInfo( + firstAgreementId + ) + + assert.equal(agreementSetup[0], partyA, 'partyA should be the creator') + assert.equal( + agreementSetup[6].toNumber(), + 0, + 'there should be no disputes' + ) + assert.equal(agreementSetup[7], false, 'there should be no disputes') + assert.equal( + agreementSetup[9].toNumber(), + 0, + 'there should be no ruling' + ) + assert.equal(agreementSetup[10], true, 'request should have executed') + + const itemSetup = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + itemSetup[0].toNumber(), + ITEM_STATUS.REGISTERED, + 'item should be in registered state' + ) + assert.isAbove( + itemSetup[1].toNumber(), + 0, + 'time of last action should be above 0' + ) + assert.equal( + itemSetup[2].toNumber(), + 0, + 'challengeRewards should have been sent back to submitter' + ) + }) + + it('should change item and agreement state for each clearing phase', async () => { + await arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + from: partyB, + value: challengeReward + }) + + const agreementId = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + const agreementBefore = await arbitrableTokenList.getAgreementInfo( + agreementId + ) + assert.equal(agreementBefore[0], partyB, 'partyB should be the creator') + assert.equal( + agreementBefore[6].toNumber(), + 0, + 'there should be no disputes' + ) + assert.equal(agreementBefore[7], false, 'there should be no disputes') + assert.equal( + agreementBefore[9].toNumber(), + 0, + 'there should be no ruling' + ) + assert.equal( + agreementBefore[10], + false, + 'request should not have executed yet' + ) + + const itemBefore = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + itemBefore[0].toNumber(), + ITEM_STATUS.CLEARING_REQUESTED, + 'item should be in clearing requested state' + ) + assert.isAbove( + itemBefore[1].toNumber(), + 0, + 'time of last action should be above zero' + ) + assert.equal( + itemBefore[2].toNumber(), + challengeReward, + 'item balance should be equal challengeReward' + ) + }) + + it('should increase and decrease contract balance', async () => { + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + 0, + 'contract should have the request reward' + ) + + await arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + from: partyB, + value: challengeReward + }) + + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + challengeReward, + 'contract should have the request reward' + ) + + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID, { from: partyA }) + + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + 0, + 'contract should have returned the fees to the submitter' + ) + }) + + describe('dispute without appeal', () => { + beforeEach(async () => { + await arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + from: partyA, + value: challengeReward + }) + }) + + it('partyA wins arbitration, item is cleared', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + from: partyB, + value: halfOfArbitrationPrice + challengeReward + }) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + challengeReward * 2 + halfOfArbitrationPrice, + 'contract shoulld have challengeReward * 2 + halfOfArbitrationPrice' + ) + await arbitrableTokenList.fundDispute(agreementID, 0, { + from: partyA, + value: halfOfArbitrationPrice + }) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + challengeReward * 2, + 'contract should only hold challengeReward * 2' + ) + const agreementBefore = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + assert.equal( + agreementBefore[1][0], + partyA, + 'side 0 should be party A' + ) + assert.equal( + agreementBefore[1][1], + partyB, + 'side 1 should be party B' + ) + assert.isTrue(agreementBefore[7], 'agreement should be disputed') + assert.isFalse(agreementBefore[8], 'agreement should not be appealed') + assert.isFalse( + agreementBefore[10], + 'agreement should have not been executed yet' + ) + + const itemBefore = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + itemBefore[0].toNumber(), + ITEM_STATUS.CLEARING_REQUESTED, + 'item should have status of clearing requested' + ) + assert.equal( + itemBefore[2].toNumber(), + challengeReward * 2, + 'item balance should hold funds from party A and B' + ) + + const partyABalanceBefore = (await web3.eth.getBalance( + partyA + )).toNumber() + const partyBBalanceBefore = (await web3.eth.getBalance( + partyB + )).toNumber() + + await expectThrow(arbitrableTokenList.executeRequest(TOKEN_ID)) // should fail since item is disputed + + // Rule in favor of partyA + await appealableArbitrator.giveRuling( + agreementBefore[6], + RULING.EXECUTE + ) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling( + agreementBefore[6], + RULING.EXECUTE + ) + + agreement = await arbitrableTokenList.getAgreementInfo(agreementID) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + 0, + 'contract should hold no balance' + ) + const agreementAfter = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + assert.equal( + agreementAfter[1][0], + partyA, + 'side 0 should still be party A' + ) + assert.equal( + agreementAfter[1][1], + partyB, + 'side 1 should still be party B' + ) + assert.isFalse( + agreementAfter[7], + 'agreement should no be disputed anymore' + ) + assert.isFalse(agreementAfter[8], 'agreement should not be appealed') + assert.isTrue( + agreementAfter[10], + 'agreement should have been executed' + ) + + const itemAfter = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + itemAfter[0].toNumber(), + ITEM_STATUS.CLEARED, + 'item should be cleared' + ) + assert.equal( + itemAfter[2].toNumber(), + 0, + 'item balance should be empty' + ) + + const partyABalanceAfter = (await web3.eth.getBalance( + partyA + )).toNumber() + const partyBBalanceAfter = (await web3.eth.getBalance( + partyB + )).toNumber() + + assert.isAtMost( + partyBBalanceAfter, + partyBBalanceBefore, + 'partyB should have not been rewarded' + ) + assert.isAbove( + partyABalanceAfter, + partyABalanceBefore, + 'partyA should have been rewarded' + ) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + 0, + 'contract funds should be 0' + ) + }) + + it('partyA looses arbitration, item is kept', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + from: partyB, + value: halfOfArbitrationPrice + challengeReward + }) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + challengeReward * 2 + halfOfArbitrationPrice, + 'contract should have challengeReward * 2 + halfOfArbitrationPrice' + ) + await arbitrableTokenList.fundDispute(agreementID, 0, { + from: partyA, + value: halfOfArbitrationPrice + }) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + challengeReward * 2, + 'contract should only hold challengeReward * 2' + ) + const agreementBefore = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + assert.equal( + agreementBefore[1][0], + partyA, + 'side 0 should be party A' + ) + assert.equal( + agreementBefore[1][1], + partyB, + 'side 1 should be party B' + ) + assert.isTrue(agreementBefore[7], 'agreement should be disputed') + assert.isFalse(agreementBefore[8], 'agreement should not be appealed') + assert.isFalse( + agreementBefore[10], + 'agreement should have not been executed yet' + ) + + const itemBefore = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + itemBefore[0].toNumber(), + ITEM_STATUS.CLEARING_REQUESTED, + 'item should have status of clearing requested' + ) + assert.equal( + itemBefore[2].toNumber(), + challengeReward * 2, + 'item balance should hold funds from party A and B' + ) + + const partyABalanceBefore = (await web3.eth.getBalance( + partyA + )).toNumber() + const partyBBalanceBefore = (await web3.eth.getBalance( + partyB + )).toNumber() + + await expectThrow(arbitrableTokenList.executeRequest(TOKEN_ID)) // should fail since item is disputed + + // Rule in favor of partyB + await appealableArbitrator.giveRuling( + agreementBefore[6], + RULING.REFUSE + ) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling( + agreementBefore[6], + RULING.REFUSE + ) + + agreement = await arbitrableTokenList.getAgreementInfo(agreementID) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + 0, + 'contract should hold no balance' + ) + const agreementAfter = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + assert.equal( + agreementAfter[1][0], + partyA, + 'side 0 should still be party A' + ) + assert.equal( + agreementAfter[1][1], + partyB, + 'side 1 should still be party B' + ) + assert.isFalse( + agreementAfter[7], + 'agreement should no be disputed anymore' + ) + assert.isFalse(agreementAfter[8], 'agreement should not be appealed') + assert.isTrue( + agreementAfter[10], + 'agreement should have been executed' + ) + + const itemAfter = await arbitrableTokenList.items(TOKEN_ID) + assert.equal( + itemAfter[0].toNumber(), + ITEM_STATUS.REGISTERED, + 'item should be registered' + ) + assert.equal( + itemAfter[2].toNumber(), + 0, + 'item balance should be empty' + ) + + const partyABalanceAfter = (await web3.eth.getBalance( + partyA + )).toNumber() + const partyBBalanceAfter = (await web3.eth.getBalance( + partyB + )).toNumber() + + assert.isAbove( + partyBBalanceAfter, + partyBBalanceBefore, + 'partyB should have been rewarded' + ) + assert.isAtMost( + partyABalanceAfter, + partyABalanceBefore, + 'partyA should have not been rewarded' + ) + assert.equal( + (await web3.eth.getBalance(arbitrableTokenList.address)).toNumber(), + 0, + 'contract funds should be 0' + ) + }) + }) + }) + + describe('item management and disputes without appeal', async () => { + beforeEach(async () => { + arbitrableTokenList = await ArbitrableTokenList.new( + appealableArbitrator.address, // arbitrator + arbitratorExtraData, + appealableArbitrator.address, // fee governor + feeStake, + t2clGovernor, + arbitrationFeesWaitingTime, + challengeReward, + timeToChallenge + ) + }) + + it('should be constructed correctly', async () => { + assert.equal( + await arbitrableTokenList.arbitrator(), + appealableArbitrator.address + ) + assert.equal( + await arbitrableTokenList.arbitratorExtraData(), + arbitratorExtraData + ) + assert.equal( + await arbitrableTokenList.challengeReward(), + challengeReward + ) + assert.equal( + await arbitrableTokenList.timeToChallenge(), + timeToChallenge + ) + }) + + describe('msg.value restrictions', async () => { + it('requestRegistration', async () => { + await expectThrow( + arbitrableTokenList.requestRegistration(TOKEN_ID, metaEvidence, { + value: challengeReward - 1 + }) + ) + }) + + it('requestClearing', async () => { + await expectThrow( + arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + value: challengeReward - 1 + }) + ) + }) + + it('challenge agreement', async () => { + await arbitrableTokenList.requestRegistration( + TOKEN_ID, + metaEvidence, + { + value: challengeReward + } + ) + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await expectThrow( + arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward - 1 + }) + ) + }) + }) + + describe('When item.disputed', function() { + beforeEach( + 'prepare pre-conditions to satisfy other requirements', + async function() { + await arbitrableTokenList.requestRegistration( + TOKEN_ID, + metaEvidence, + { + value: challengeReward + } + ) // To satisfy `require(item.status==ItemStatus.Resubmitted || item.status==ItemStatus.Submitted)` + + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + await arbitrableTokenList.fundDispute(agreementID, 0, { + value: halfOfArbitrationPrice + }) // To dissatisfy `require(!item.disputed)` + } + ) + + beforeEach('assert pre-conditions', async function() { + assert.ok( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber() === + ITEM_STATUS.SUBMITTED || + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber() === + ITEM_STATUS.RESUBMITTED + ) + + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + assert.equal(agreement[7], true, 'agreement should be disputed') + }) + + it('registration dispute', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await expectThrow( + arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + ) + }) + + it('clearing dispute', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await expectThrow( + arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + ) + }) + }) + + describe('When !(item.status==ItemStatus.ClearingRequested || item.status==ItemStatus.PreventiveClearingRequested))', function() { + beforeEach('assert pre-conditions', async function() { + assert.ok( + (await arbitrableTokenList.items(TOKEN_ID))[0] < + ITEM_STATUS.CLEARING_REQUESTED + ) + }) + + it('registration dispute', async function() { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await expectThrow( + arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + ) + }) + + it('clearing dispute', async function() { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await expectThrow( + arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + ) + }) + }) + + describe('When item in absent state', function() { + beforeEach('assert pre-conditions', async function() { + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0], + ITEM_STATUS.ABSENT + ) + }) + + it('calling isPermitted should return false', async () => { + assert.equal(await arbitrableTokenList.isPermitted(TOKEN_ID), false) + }) + + it('calling requestRegistration should move item into the submitted state', async () => { + await arbitrableTokenList.requestRegistration( + TOKEN_ID, + metaEvidence, + { + value: challengeReward + } + ) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0], + ITEM_STATUS.SUBMITTED + ) + }) + + it('calling requestClearing should move item into the preventive clearing requested state', async () => { + await arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + value: challengeReward + }) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.PREVENTIVE_CLEARING_REQUESTED + ) + }) + + it('calling challangeBlacklisting should revert', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await expectThrow( + arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + ) + }) + + it('calling challangeClearing should revert', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await expectThrow( + arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + ) + }) + + it('calling executeRequest should revert', async () => { + await expectThrow(arbitrableTokenList.executeRequest(TOKEN_ID)) + }) + }) + + describe('When item in cleared state', function() { + beforeEach('prepare pre-conditions', async function() { + await arbitrableTokenList.requestRegistration( + TOKEN_ID, + metaEvidence, + { + value: challengeReward + } + ) + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID) + await arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + value: challengeReward + }) + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID) + }) + + beforeEach('assert pre-conditions', async function() { + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0], + ITEM_STATUS.CLEARED + ) + }) + + it('calling isPermitted should return false', async () => { + assert.equal(await arbitrableTokenList.isPermitted(TOKEN_ID), false) + }) + + it('calling requestRegistration should move item into the resubmitted state', async () => { + await arbitrableTokenList.requestRegistration( + TOKEN_ID, + metaEvidence, + { + value: challengeReward + } + ) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.RESUBMITTED + ) + }) + + it('calling requestClearing should revert', async () => { + await expectThrow( + arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + value: challengeReward + }) + ) + }) + + it('calling challangeClearing should revert', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await expectThrow( + arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + ) + }) + + it('calling executeRequest should revert', async () => { + await expectThrow(arbitrableTokenList.executeRequest(TOKEN_ID)) + }) + }) + + describe('When item in resubmitted state', function() { + beforeEach('prepare pre-conditions', async function() { + await arbitrableTokenList.requestRegistration( + TOKEN_ID, + metaEvidence, + { + from: partyA, + value: challengeReward + } + ) + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID, { + from: partyA + }) + await arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + from: partyB, + value: challengeReward + }) + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID, { + from: partyB + }) + await arbitrableTokenList.requestRegistration( + TOKEN_ID, + metaEvidence, + { + from: partyA, + value: challengeReward + } + ) + }) + + beforeEach('assert pre-conditions', async function() { + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.RESUBMITTED + ) + }) + + it('calling isPermitted should return true', async () => { + assert.equal(await arbitrableTokenList.isPermitted(TOKEN_ID), false) + }) + + it('calling requestRegistration should revert', async () => { + await expectThrow( + arbitrableTokenList.requestRegistration(TOKEN_ID, metaEvidence, { + value: challengeReward + }) + ) + }) + + it('calling requestClearing should revert', async function() { + await expectThrow( + arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + value: challengeReward + }) + ) + }) + + it('calling fundDispute should create a dispute', async function() { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + await arbitrableTokenList.fundDispute(agreementID, 0, { + value: halfOfArbitrationPrice + }) + + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + assert.equal(agreement[1][1].toString(), governor) + assert.equal(agreement[7], true, 'agreement should be disputed') + assert.equal( + web3.toUtf8( + await arbitrableTokenList.agreementIDToItemID(agreementID) + ), + TOKEN_ID + ) + }) + + describe('executeRuling', async function() { + let disputeID + + beforeEach('create a dispute', async function() { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice, + from: partyB + }) + await arbitrableTokenList.fundDispute(agreementID, 0, { + value: halfOfArbitrationPrice + }) + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + + disputeID = agreement[6].toNumber() + + assert.notEqual( + agreement[1][0], + agreement[1][1], + 'subitter and challenger should be different' + ) + }) + + it('calling executeRuling with EXECUTE should send item.balance to submitter', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const submitter = agreement[1][0] + const submitterBalance = web3.eth.getBalance(submitter) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + + await appealableArbitrator.giveRuling(disputeID, RULING.EXECUTE) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.EXECUTE) + + const actualBalanceOfSubmitter = web3.eth.getBalance(submitter) + const expectedBalanceOfSubmitter = submitterBalance.plus( + itemBalance + ) + + assert( + actualBalanceOfSubmitter.equals(expectedBalanceOfSubmitter), + 'Difference: ' + + actualBalanceOfSubmitter + .minus(expectedBalanceOfSubmitter) + .toNumber() + ) + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.REGISTERED + ) + }) + + it('calling executeRuling with REFUSE should send item.balance to challenger', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const challenger = agreement[1][1] + const challengerBalance = web3.eth.getBalance(challenger) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + + await appealableArbitrator.giveRuling(disputeID, RULING.REFUSE) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.REFUSE) + + const actualBalanceOfChallenger = web3.eth.getBalance(challenger) + const expectedBalanceOfChallenger = itemBalance.plus( + challengerBalance + ) + + assert( + actualBalanceOfChallenger.equals(expectedBalanceOfChallenger), + 'Difference: ' + + actualBalanceOfChallenger + .minus(expectedBalanceOfChallenger) + .toNumber() + ) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.CLEARED + ) + }) + + it('calling executeRuling with OTHER should split item.balance between challenger and submitter and move item into the cleared state', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const submitter = agreement[1][0] + const challenger = agreement[1][1] + const submitterBalance = web3.eth.getBalance(submitter) + const challengerBalance = web3.eth.getBalance(challenger) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + + await appealableArbitrator.giveRuling(disputeID, RULING.OTHER) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.OTHER) + + const actualBalanceOfSubmitter = web3.eth.getBalance(submitter) + const actualBalanceOfChallenger = web3.eth.getBalance(challenger) + const expectedBalanceOfSubmitter = itemBalance + .dividedBy(new BigNumber(2)) + .plus(submitterBalance) + + const expectedBalanceOfChallenger = itemBalance + .dividedBy(new BigNumber(2)) + .plus(challengerBalance) + + assert( + actualBalanceOfSubmitter.equals(expectedBalanceOfSubmitter), + 'Actual: ' + + actualBalanceOfSubmitter + + '\t0Expected: ' + + expectedBalanceOfSubmitter + ) + assert( + actualBalanceOfChallenger.equals(expectedBalanceOfChallenger), + '1Differece: ' + + actualBalanceOfChallenger.minus(expectedBalanceOfChallenger) + ) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.CLEARED + ) + }) + }) + }) + + describe('When item in registered state', function() { + beforeEach('prepare pre-conditions', async function() { + await arbitrableTokenList.requestRegistration( + TOKEN_ID, + metaEvidence, + { + value: challengeReward + } + ) + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID) + }) + + beforeEach('assert pre-conditions', async function() { + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0], + ITEM_STATUS.REGISTERED + ) + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + assert.isTrue(agreement[10], 'agreement should be executed') + }) + + it('calling isPermitted should return true', async () => { + assert.equal(await arbitrableTokenList.isPermitted(TOKEN_ID), true) + }) + + it('calling requestRegistration should revert', async () => { + await expectThrow( + arbitrableTokenList.requestRegistration(TOKEN_ID, metaEvidence, { + value: challengeReward + }) + ) + }) + + it('calling requestClearing should move item into the clearing requested state', async () => { + await arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + value: challengeReward + }) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.CLEARING_REQUESTED + ) + }) + + it('calling fund dispute over executed agreement should revert', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await expectThrow( + arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + ) + }) + + it('calling clearing dispute should revert', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await expectThrow( + arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + ) + }) + + it('calling executeRequest should revert', async function() { + await expectThrow(arbitrableTokenList.executeRequest(TOKEN_ID)) + }) + }) + + describe('When item in submitted state', function() { + beforeEach('prepare pre-conditions', async function() { + await arbitrableTokenList.requestRegistration( + TOKEN_ID, + metaEvidence, + { + value: challengeReward, + from: partyA + } + ) + }) + + beforeEach('assert pre-conditions', async function() { + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.SUBMITTED + ) + }) + + it('calling isPermitted should return false', async () => { + assert.equal(await arbitrableTokenList.isPermitted(TOKEN_ID), false) + }) + + it('calling requestRegistration should revert', async () => { + await expectThrow( + arbitrableTokenList.requestRegistration(TOKEN_ID, metaEvidence, { + value: challengeReward + }) + ) + }) + + it('calling requestClearing should move item into the clearing requested state', async () => { + await expectThrow( + arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + value: challengeReward + }) + ) + }) + + it('calling challangeBlacklisting should create a dispute', async function() { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + await arbitrableTokenList.fundDispute(agreementID, 0, { + value: halfOfArbitrationPrice + }) + + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + + assert.equal(agreement[1][1], governor) + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[2].toNumber(), + challengeReward * 2 + ) + assert.equal(agreement[7], true, 'item should be disputed') + assert.equal( + web3.toUtf8( + await arbitrableTokenList.agreementIDToItemID(agreementID) + ), + TOKEN_ID + ) + }) + + it('calling executeRequest should move item into the registered state', async function() { + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.REGISTERED + ) + }) + + describe('executeRuling', async function() { + let disputeID + + beforeEach('create a dispute', async function() { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice, + from: partyB + }) + await arbitrableTokenList.fundDispute(agreementID, 0, { + value: halfOfArbitrationPrice, + from: partyA + }) + + disputeID = (await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ))[6].toNumber() + }) + + beforeEach('assert pre-conditions', async () => { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + assert.notEqual( + agreement[1][0], + agreement[1][1], + 'subitter and challenger should be different' + ) + }) + + it('calling executeRuling with EXECUTE should send item.balance to submitter', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const submitter = agreement[0] + const submitterBalance = web3.eth.getBalance(submitter) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + + await appealableArbitrator.giveRuling(disputeID, RULING.EXECUTE) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.EXECUTE) + + const actualBalanceOfSubmitter = web3.eth.getBalance(submitter) + const expectedBalanceOfSubmitter = itemBalance.plus( + submitterBalance + ) + + const expectedItemStatus = ITEM_STATUS.REGISTERED + + assert( + actualBalanceOfSubmitter.equals(expectedBalanceOfSubmitter), + 'Actual: ' + + actualBalanceOfSubmitter + + '\tExpected: ' + + expectedBalanceOfSubmitter + ) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + expectedItemStatus + ) + }) + + it('calling executeRuling with REFUSE should send item.balance to challenger', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const challenger = agreement[1][1] + const challengerBalance = web3.eth.getBalance(challenger) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + + await appealableArbitrator.giveRuling(disputeID, RULING.REFUSE) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.REFUSE) + + const actualBalanceOfChallenger = web3.eth.getBalance(challenger) + const expectedBalanceOfChallenger = challengerBalance.plus( + itemBalance + ) + + assert( + actualBalanceOfChallenger.equals(expectedBalanceOfChallenger), + 'Actual: ' + + actualBalanceOfChallenger + + '\tExpected: ' + + expectedBalanceOfChallenger + ) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.CLEARED + ) + }) + + it('calling executeRuling with OTHER should split item.balance between challenger and submitter and move item into the absent state', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const submitter = agreement[1][0] + const challenger = agreement[1][1] + const submitterBalance = web3.eth.getBalance(submitter) + const challengerBalance = web3.eth.getBalance(challenger) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + const disputeID = agreement[6] + + await appealableArbitrator.giveRuling(disputeID, RULING.OTHER) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.OTHER) + + const actualBalanceOfSubmitter = web3.eth.getBalance(submitter) + const actualBalanceOfChallenger = web3.eth.getBalance(challenger) + const expectedBalanceOfSubmitter = itemBalance + .dividedBy(new BigNumber(2)) + .plus(submitterBalance) + + const expectedBalanceOfChallenger = itemBalance + .dividedBy(new BigNumber(2)) + .plus(challengerBalance) + + assert( + actualBalanceOfSubmitter.equals(expectedBalanceOfSubmitter), + 'Actual: ' + + actualBalanceOfSubmitter + + '\tExpected: ' + + expectedBalanceOfSubmitter + ) + assert( + actualBalanceOfChallenger.equals(expectedBalanceOfChallenger), + 'Actual: ' + + actualBalanceOfChallenger + + '\tExpected: ' + + expectedBalanceOfChallenger + ) + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.ABSENT + ) + }) + }) + }) + + describe('When item in clearing requested state', function() { + beforeEach('prepare pre-conditions', async function() { + await arbitrableTokenList.requestRegistration( + TOKEN_ID, + metaEvidence, + { + from: partyA, + value: challengeReward + } + ) + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID, { + from: partyA + }) + await arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + from: partyB, + value: challengeReward + }) + }) + + beforeEach('assert pre-conditions', async function() { + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.CLEARING_REQUESTED + ) + }) + + it('calling isPermitted should return true', async () => { + assert.equal(await arbitrableTokenList.isPermitted(TOKEN_ID), true) + }) + + it('calling requestRegistration should revert', async () => { + await expectThrow( + arbitrableTokenList.requestRegistration(TOKEN_ID, metaEvidence, { + from: partyA, + value: challengeReward + }) + ) + }) + + it('calling requestClearing should revert', async function() { + await expectThrow( + arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + from: partyB, + value: challengeReward + }) + ) + }) + + it('calling challangeClearing should create a dispute', async function() { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + from: partyA, + value: challengeReward + halfOfArbitrationPrice + }) + await arbitrableTokenList.fundDispute(agreementID, 0, { + from: partyB, + value: halfOfArbitrationPrice + }) + + const agreement = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + + assert.equal(agreement[1][1], partyA) + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[2].toNumber(), + challengeReward * 2 + ) + assert.equal(agreement[7], true) + assert.equal( + web3.toUtf8( + await arbitrableTokenList.agreementIDToItemID(agreementID) + ), + TOKEN_ID + ) + }) + + it('calling executeRequest should move item into the cleared state', async function() { + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID, { + from: partyA + }) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.CLEARED + ) + }) + + describe('executeRuling', async function() { + let disputeID + + beforeEach('create a dispute', async function() { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice, + from: partyA + }) + await arbitrableTokenList.fundDispute(agreementID, 0, { + value: halfOfArbitrationPrice + }) + const agreement = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + disputeID = agreement[6].toNumber() + }) + + it('calling executeRuling with REFUSE should send item.balance to challenger', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const challenger = agreement[1][1] + const challengerBalance = web3.eth.getBalance(challenger) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + + await appealableArbitrator.giveRuling(disputeID, RULING.REFUSE) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.REFUSE) + + const actualBalanceOfChallenger = web3.eth.getBalance(challenger) + const expectedBalanceOfChallenger = challengerBalance.plus( + itemBalance + ) + + assert( + actualBalanceOfChallenger.equals(expectedBalanceOfChallenger), + 'Difference: ' + + actualBalanceOfChallenger.minus(expectedBalanceOfChallenger) + ) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.REGISTERED + ) + }) + + it('calling executeRuling with EXECUTE should send item.balance to submitter', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const submitter = agreement[1][0] + const submitterBalance = web3.eth.getBalance(submitter) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + + await appealableArbitrator.giveRuling(disputeID, RULING.EXECUTE) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.EXECUTE) + + const actualBalanceOfSubmitter = web3.eth.getBalance(submitter) + const expectedBalanceOfSubmitter = submitterBalance.plus( + itemBalance + ) + + assert( + actualBalanceOfSubmitter.equals(expectedBalanceOfSubmitter), + 'Difference: ' + + actualBalanceOfSubmitter.minus(expectedBalanceOfSubmitter) + ) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.CLEARED + ) + }) + + it('calling executeRuling with OTHER should split item.balance between challenger and submitter and move item into the registered state', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const submitter = agreement[1][0] + const challenger = agreement[1][1] + const submitterBalance = web3.eth.getBalance(submitter) + const challengerBalance = web3.eth.getBalance(challenger) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + const disputeID = agreement[6] + + await appealableArbitrator.giveRuling(disputeID, RULING.OTHER) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.OTHER) + + const actualBalanceOfSubmitter = web3.eth.getBalance(submitter) + const actualBalanceOfChallenger = web3.eth.getBalance(challenger) + const expectedBalanceOfSubmitter = itemBalance + .dividedBy(2) + .plus(submitterBalance) + const expectedBalanceOfChallenger = itemBalance + .dividedBy(2) + .plus(challengerBalance) + + assert( + actualBalanceOfSubmitter.equals(expectedBalanceOfSubmitter), + 'Difference: ' + + actualBalanceOfSubmitter.minus(expectedBalanceOfSubmitter) + ) + assert( + actualBalanceOfChallenger.equals(expectedBalanceOfChallenger), + 'Difference: ' + + actualBalanceOfChallenger.minus(expectedBalanceOfChallenger) + ) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.REGISTERED + ) + }) + }) + }) + + describe('When item in preventive clearing requested state', function() { + beforeEach('prepare pre-conditions', async function() { + await arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + from: partyB, + value: challengeReward + }) + }) + + beforeEach('assert pre-conditions', async function() { + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.PREVENTIVE_CLEARING_REQUESTED + ) + }) + + it('calling isPermitted on a not-disputed item should return false', async () => { + assert.equal(await arbitrableTokenList.isPermitted(TOKEN_ID), false) + }) + + it('calling isPermitted on a disputed item should return false', async () => { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice + }) + await arbitrableTokenList.fundDispute(agreementID, 0, { + value: halfOfArbitrationPrice + }) // To satisfy disputed pre-condition + + assert.equal(await arbitrableTokenList.isPermitted(TOKEN_ID), false) + }) + + it('calling requestRegistration should revert', async () => { + await expectThrow( + arbitrableTokenList.requestRegistration(TOKEN_ID, metaEvidence, { + from: partyA, + value: challengeReward + }) + ) + }) + + it('calling requestClearing should revert', async function() { + await expectThrow( + arbitrableTokenList.requestClearing(TOKEN_ID, metaEvidence, { + from: partyB, + value: challengeReward + }) + ) + }) + + it('calling challangeClearing should create a dispute', async function() { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice, + from: partyA + }) + await arbitrableTokenList.fundDispute(agreementID, 0, { + value: halfOfArbitrationPrice + }) + + const agreement = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + + assert.equal(agreement[1][1], partyA) + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[2].toNumber(), + challengeReward * 2 + ) + assert.equal(agreement[7], true) + assert.equal( + web3.toUtf8( + await arbitrableTokenList.agreementIDToItemID(agreementID) + ), + TOKEN_ID + ) + }) + + it('calling executeRequest should move item into the cleared state', async function() { + await increaseTime(1) + await arbitrableTokenList.executeRequest(TOKEN_ID) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.CLEARED + ) + }) + + describe('executeRuling', async function() { + let disputeID + + beforeEach('create a dispute', async function() { + const agreementID = await arbitrableTokenList.latestAgreementID( + TOKEN_ID + ) + const agreement = await arbitrableTokenList.getAgreementInfo( + agreementID + ) + await arbitrableTokenList.fundDispute(agreementID, 1, { + value: challengeReward + halfOfArbitrationPrice, + from: partyA + }) + await arbitrableTokenList.fundDispute(agreementID, 0, { + value: halfOfArbitrationPrice + }) + + disputeID = agreement[6].toNumber() + }) + + it('calling executeRuling with REFUSE should send item.balance to challenger', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const challenger = agreement[1][1] + const challengerBalance = web3.eth.getBalance(challenger) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + + await appealableArbitrator.giveRuling(disputeID, RULING.REFUSE) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.REFUSE) + + const actualBalanceOfChallenger = web3.eth.getBalance(challenger) + const expectedBalanceOfChallenger = challengerBalance.plus( + itemBalance + ) + + assert( + actualBalanceOfChallenger.equals(expectedBalanceOfChallenger) + ) + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.REGISTERED + ) + }) + + it('calling executeRuling with EXECUTE should send item.balance to submitter', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const submitter = agreement[1][0] + const submitterBalance = web3.eth.getBalance(submitter) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + + await appealableArbitrator.giveRuling(disputeID, RULING.EXECUTE) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.EXECUTE) + + const actualBalanceOfSubmitter = web3.eth.getBalance(submitter) + const expectedBalanceOfSubmitter = itemBalance.plus( + submitterBalance + ) + + assert(actualBalanceOfSubmitter.equals(expectedBalanceOfSubmitter)) + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.CLEARED + ) + }) + + it('calling executeRuling with OTHER should split item.balance between challenger and submitter and move item into the absent state', async function() { + const agreement = await arbitrableTokenList.getAgreementInfo( + await arbitrableTokenList.latestAgreementID(TOKEN_ID) + ) + const submitter = agreement[1][0] + const challenger = agreement[1][1] + const submitterBalance = web3.eth.getBalance(submitter) + const challengerBalance = web3.eth.getBalance(challenger) + const itemBalance = (await arbitrableTokenList.items(TOKEN_ID))[2] + const disputeID = agreement[6] + + await appealableArbitrator.giveRuling(disputeID, RULING.OTHER) + await increaseTime(appealPeriodDuration + 1) + await appealableArbitrator.giveRuling(disputeID, RULING.OTHER) + + const actualBalanceOfSubmitter = web3.eth.getBalance(submitter) + const actualBalanceOfChallenger = web3.eth.getBalance(challenger) + const expectedBalanceOfSubmitter = itemBalance + .dividedBy(2) + .plus(submitterBalance) + + const expectedBalanceOfChallenger = itemBalance + .dividedBy(2) + .plus(challengerBalance) + + assert( + actualBalanceOfSubmitter.equals(expectedBalanceOfSubmitter), + 'Difference: ' + + actualBalanceOfSubmitter.minus(expectedBalanceOfSubmitter) + ) + assert( + actualBalanceOfChallenger.equals(expectedBalanceOfChallenger), + 'Difference: ' + + actualBalanceOfChallenger.minus(expectedBalanceOfChallenger) + ) + + assert.equal( + (await arbitrableTokenList.items(TOKEN_ID))[0].toNumber(), + ITEM_STATUS.ABSENT + ) + }) + }) + }) + }) + }) +})