Implementing an Arbitrable

Warning

Smart contracts in this tutorial are not intended for production but educational purposes. Beware of using them on main network.

When developing arbitrable contracts, we need to:

  • Implement rule function to define an action to be taken when a ruling is received by the contract.
  • Develop a logic to create disputes (via calling createDispute on Arbitrable)

Consider a case where two parties trade ether for goods. Payer wants to pay only if payee provides promised goods. So payer deposits payment amount into an escrow and if a dispute arises an arbitrator will resolve it.

There will be two scenarios:
  1. No dispute arises, payee withdraws the funds.
  2. payer reclaims funds by depositing arbitration fee…
    1. payee fails to deposit arbitration fee in arbitrationFeeDepositPeriod and payer wins by default. The arbitration fee deposit paid by payer refunded.
    2. payee deposits arbitration fee in time. Dispute gets created. arbitrator rules. Winner gets the arbitration fee refunded.

Notice that only in scenario 2b arbitrator intervenes. In other scenarios we don’t create a dispute thus don’t await for a ruling. Also, notice that in case of a dispute, the winning side gets reimbursed for the arbitration fee deposit. So in effect, the loser will be paying for the arbitration.

Let’s start:

pragma solidity ^0.5;

import "../IArbitrable.sol";
import "../IArbitrator.sol";

contract SimpleEscrow is IArbitrable {
    address payable public payer = msg.sender;
    address payable public payee;
    uint public value;
    IArbitrator public arbitrator;
    string public agreement;
    uint public createdAt;
    uint constant public reclamationPeriod = 3 minutes;
    uint constant public arbitrationFeeDepositPeriod = 3 minutes;

    constructor(address payable _payee, IArbitrator _arbitrator, string memory _agreement) public payable {
        value = msg.value;
        payee = _payee;
        arbitrator = _arbitrator;
        agreement = _agreement;
        createdAt = now;

payer deploys the contract depositing the payment amount and specifying payee address, arbitrator that is authorized to rule and agreement string. Notice that payer = msg.sender.

We made reclamationPeriod and arbitrationFeeDepositPeriod constant for sake of simplicity, they could be set by payer in the constructor too.

Let’s implement the first scenario:

pragma solidity ^0.5;

import "../IArbitrable.sol";
import "../IArbitrator.sol";

contract SimpleEscrow is IArbitrable {
    address payable public payer = msg.sender;
    address payable public payee;
    uint public value;
    IArbitrator public arbitrator;
    string public agreement;
    uint public createdAt;
    uint constant public reclamationPeriod = 3 minutes;
    uint constant public arbitrationFeeDepositPeriod = 3 minutes;


    enum Status {Initial, Reclaimed, Disputed, Resolved}
    Status public status;


    constructor(address payable _payee, IArbitrator _arbitrator, string memory _agreement) public payable {
        value = msg.value;
        payee = _payee;
        arbitrator = _arbitrator;
        agreement = _agreement;
        createdAt = now;
    }

    function releaseFunds() public {
      require(status == Status.Initial, "Transaction is not in Initial state.");

      if(msg.sender != payer)
          require(now - createdAt > reclamationPeriod, "Payer still has time to reclaim.");

      status = Status.Resolved;
      payee.send(value);
    }

In releaseFunds function, first we do state checks: transaction should be in Status.Initial and reclamationPeriod should be passed unless the caller is payer. If so, we update status to Status.Resolved and send the funds to payee.

Moving forward to second scenario:

pragma solidity ^0.5;

import "../IArbitrable.sol";
import "../IArbitrator.sol";

contract SimpleEscrow is IArbitrable {
    address payable public payer = msg.sender;
    address payable public payee;
    uint public value;
    IArbitrator public arbitrator;
    string public agreement;
    uint public createdAt;
    uint constant public reclamationPeriod = 3 minutes;
    uint constant public arbitrationFeeDepositPeriod = 3 minutes;


    enum Status {Initial, Reclaimed, Disputed, Resolved}
    Status public status;

    uint public reclaimedAt;

    enum RulingOptions {RefusedToArbitrate, PayerWins, PayeeWins}
    uint constant numberOfRulingOptions = 2; // Notice that option 0 is reserved for RefusedToArbitrate.

    constructor(address payable _payee, IArbitrator _arbitrator, string memory _agreement) public payable {
        value = msg.value;
        payee = _payee;
        arbitrator = _arbitrator;
        agreement = _agreement;
        createdAt = now;
    }

    function releaseFunds() public {
      require(status == Status.Initial, "Transaction is not in Initial state.");

      if(msg.sender != payer)
          require(now - createdAt > reclamationPeriod, "Payer still has time to reclaim.");

      status = Status.Resolved;
      payee.send(value);
    }

    function reclaimFunds() public payable {
        require(status == Status.Initial || status == Status.Reclaimed, "Transaction is not in Initial or Reclaimed state.");
        require(msg.sender == payer, "Only the payer can reclaim the funds.");

        if (status == Status.Reclaimed){
            require(now - reclaimedAt > arbitrationFeeDepositPeriod, "Payee still has time to deposit arbitration fee.");
            payer.send(address(this).balance);
            status = Status.Resolved;
        }
        else {
          require(now - createdAt <= reclamationPeriod, "Reclamation period ended.");
          require(msg.value == arbitrator.arbitrationCost(""), "Can't reclaim funds without depositing arbitration fee.");
          reclaimedAt = now;
          status = Status.Reclaimed;
        }
    }

    function depositArbitrationFeeForPayee() public payable {
        require(status == Status.Reclaimed, "Transaction is not in Reclaimed state.");
        arbitrator.createDispute.value(msg.value)(numberOfRulingOptions, "");
        status = Status.Disputed;
    }

    function rule(uint _disputeID, uint _ruling) public {
        require(msg.sender == address(arbitrator), "Only the arbitrator can execute this.");
        require(status == Status.Disputed, "There should be dispute to execute a ruling.");
        require(_ruling <= numberOfRulingOptions, "Ruling out of bounds!");

        status = Status.Resolved;
        if (_ruling == uint(RulingOptions.PayerWins)) payer.send(address(this).balance);
        else if (_ruling == uint(RulingOptions.PayeeWins)) payee.send(address(this).balance);
        emit Ruling(arbitrator, _disputeID, _ruling);
    }

    function remainingTimeToReclaim() public view returns (uint) {
        if (status != Status.Initial) revert("Transaction is not in Initial state.");
        return (createdAt + reclamationPeriod - now) > reclamationPeriod ? 0 : (createdAt + reclamationPeriod - now);
    }

    function remainingTimeToDepositArbitrationFee() public view returns (uint) {
        if (status != Status.Reclaimed) revert("Transaction is not in Reclaimed state.");
        return (reclaimedAt + arbitrationFeeDepositPeriod - now) > arbitrationFeeDepositPeriod ? 0 : (reclaimedAt + arbitrationFeeDepositPeriod - now);
    }
}

reclaimFunds function lets payer to reclaim their funds. After payer calls this function for the first time the window (arbitrationFeeDepositPeriod) for payee to deposit arbitration fee starts. If they fail to deposit in time, payer can call the function for the second time and get the funds back. In case if payee deposits the arbitration fee in time a dispute gets created and the contract awaits arbitrator’s decision.

We define enforcement of rulings in rule function. Whoever wins the dispute should get the funds and should get reimbursed for the arbitration fee. Recall that we took the arbitration fee deposit from both sides and used one of them to pay for the arbitrator. Thus the balance of the contract is at least funds plus arbitration fee. Therefore we send address(this).balance to the winner. Lastly, we emit Ruling as required in the standard.

And also we have two view functions to get remaining times, which will be useful for front-end development.

That’s it! We implemented a very simple escrow using ERC-792.