Implementing an Arbitrator

Warning

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

When developing arbitrator contracts we need to:

  • Implement the functions createDispute and appeal. Don’t forget to store the arbitrated contract and the disputeID (which should be unique).
  • Implement the functions for cost display (arbitrationCost and appealCost).
  • Allow enforcing rulings. For this a function must execute arbitrable.rule(disputeID, ruling).

To demonstrate how to use the standard, we will implement a very simple arbitrator where a single address gives rulings and there aren’t any appeals.

Let’s start by implementing cost functions:

pragma solidity ^0.5;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {

    function arbitrationCost(bytes memory _extraData) public view returns(uint fee) {
        fee = 0.1 ether;
    }

    function appealCost(uint _disputeID, bytes memory _extraData) public view returns(uint fee) {
        fee = 2**250; // An unaffordable amount which practically avoids appeals.
    }

We set the arbitration fee to 0.1 ether and the appeal fee to an astronomical amount which can’t be afforded. So in practice, we disabled appeal, for simplicity. We made costs constant, again, for the sake of simplicity of this tutorial.

Next, we need a data structure to keep track of disputes:

pragma solidity ^0.5;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {

    struct Dispute {
        IArbitrable arbitrated;
        uint choices;
        uint ruling;
        DisputeStatus status;
    }

    Dispute[] public disputes;

    function arbitrationCost(bytes memory _extraData) public view returns(uint fee) {
        fee = 0.1 ether;
    }

    function appealCost(uint _disputeID, bytes memory _extraData) public view returns(uint fee) {
        fee = 2**250; // An unaffordable amount which practically avoids appeals.
    }

Each dispute belongs to an Arbitrable contract, so we have arbitrated field for it. Each dispute will have a ruling stored in ruling field: For example, Party A wins (represented by ruling = 1) and Party B wins (represented by ruling = 2), recall that ruling = 0 is reserved for “refused to arbitrate”. We also store number of ruling options in choices to be able to avoid undefined rulings in the proxy function which executes arbitrable.rule(disputeID, ruling). Finally, each dispute will have a status, and we store it inside status field.

Next, we can implement the function for creating disputes:

pragma solidity ^0.5;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {

    struct Dispute {
        IArbitrable arbitrated;
        uint choices;
        uint ruling;
        DisputeStatus status;
    }

    Dispute[] public disputes;

    function arbitrationCost(bytes memory _extraData) public view returns(uint fee) {
        fee = 0.1 ether;
    }

    function appealCost(uint _disputeID, bytes memory _extraData) public view returns(uint fee) {
        fee = 2**250; // An unaffordable amount which practically avoids appeals.
    }

    function createDispute(uint _choices, bytes memory _extraData) public payable returns(uint disputeID) {
        require(msg.value >= arbitrationCost(_extraData), "Not enough ETH to cover arbitration costs.");

        disputeID = disputes.push(Dispute({
          arbitrated: IArbitrable(msg.sender),
          choices: _choices,
          ruling: uint(-1),
          status: DisputeStatus.Waiting
          })) -1;

        emit DisputeCreation(disputeID, IArbitrable(msg.sender));
    }

Note that createDispute function should be called by an arbitrable.

We require the caller to pay at least arbitrationCost(_extraData). We could send back the excess payment, but we omitted it for the sake of simplicity.

Then, we create the dispute by pushing a new element to the array: disputes.push( ... ). The push function returns the resulting size of the array, thus we can use the return value of disputes.push( ... ) -1 as disputeID starting from zero. Finally, we emit DisputeCreation as required in the standard.

We also need to implement getters for status and ruling:

pragma solidity ^0.5;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {

    struct Dispute {
        IArbitrable arbitrated;
        uint choices;
        uint ruling;
        DisputeStatus status;
    }

    Dispute[] public disputes;

    function arbitrationCost(bytes memory _extraData) public view returns(uint fee) {
        fee = 0.1 ether;
    }

    function appealCost(uint _disputeID, bytes memory _extraData) public view returns(uint fee) {
        fee = 2**250; // An unaffordable amount which practically avoids appeals.
    }

    function createDispute(uint _choices, bytes memory _extraData) public payable returns(uint disputeID) {
        require(msg.value >= arbitrationCost(_extraData), "Not enough ETH to cover arbitration costs.");

        disputeID = disputes.push(Dispute({
          arbitrated: IArbitrable(msg.sender),
          choices: _choices,
          ruling: uint(-1),
          status: DisputeStatus.Waiting
          })) -1;

        emit DisputeCreation(disputeID, IArbitrable(msg.sender));
    }

    function disputeStatus(uint _disputeID) public view returns(DisputeStatus status) {
        status = disputes[_disputeID].status;
    }

    function currentRuling(uint _disputeID) public view returns(uint ruling) {
        ruling = disputes[_disputeID].ruling;
    }

Finally, we need a proxy function to call rule function of the Arbitrable contract. In this simple Arbitrator we will let one address, the creator of the contract, to give rulings. So let’s start by storing contract creator’s address:

pragma solidity ^0.5;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {

    address public owner = msg.sender;

    struct Dispute {
        IArbitrable arbitrated;
        uint choices;
        uint ruling;
        DisputeStatus status;
    }

    Dispute[] public disputes;

    function arbitrationCost(bytes memory _extraData) public view returns(uint fee) {
        fee = 0.1 ether;
    }

    function appealCost(uint _disputeID, bytes memory _extraData) public view returns(uint fee) {
        fee = 2**250; // An unaffordable amount which practically avoids appeals.
    }

    function createDispute(uint _choices, bytes memory _extraData) public payable returns(uint disputeID) {
        require(msg.value >= arbitrationCost(_extraData), "Not enough ETH to cover arbitration costs.");

        disputeID = disputes.push(Dispute({
          arbitrated: IArbitrable(msg.sender),
          choices: _choices,
          ruling: uint(-1),
          status: DisputeStatus.Waiting
          })) -1;

        emit DisputeCreation(disputeID, IArbitrable(msg.sender));
    }

    function disputeStatus(uint _disputeID) public view returns(DisputeStatus status) {
        status = disputes[_disputeID].status;
    }

    function currentRuling(uint _disputeID) public view returns(uint ruling) {
        ruling = disputes[_disputeID].ruling;
    }

Then the proxy function:

pragma solidity ^0.5;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {

    address public owner = msg.sender;

    struct Dispute {
        IArbitrable arbitrated;
        uint choices;
        uint ruling;
        DisputeStatus status;
    }

    Dispute[] public disputes;

    function arbitrationCost(bytes memory _extraData) public view returns(uint fee) {
        fee = 0.1 ether;
    }

    function appealCost(uint _disputeID, bytes memory _extraData) public view returns(uint fee) {
        fee = 2**250; // An unaffordable amount which practically avoids appeals.
    }

    function createDispute(uint _choices, bytes memory _extraData) public payable returns(uint disputeID) {
        require(msg.value >= arbitrationCost(_extraData), "Not enough ETH to cover arbitration costs.");

        disputeID = disputes.push(Dispute({
          arbitrated: IArbitrable(msg.sender),
          choices: _choices,
          ruling: uint(-1),
          status: DisputeStatus.Waiting
          })) -1;

        emit DisputeCreation(disputeID, IArbitrable(msg.sender));
    }

    function disputeStatus(uint _disputeID) public view returns(DisputeStatus status) {
        status = disputes[_disputeID].status;
    }

    function currentRuling(uint _disputeID) public view returns(uint ruling) {
        ruling = disputes[_disputeID].ruling;
    }

    function rule(uint _disputeID, uint _ruling) public {
        require(msg.sender == owner, "Only the owner of this contract can execute rule function.");

        Dispute storage dispute = disputes[_disputeID];

        require(_ruling <= dispute.choices, "Ruling out of bounds!");
        require(dispute.status == DisputeStatus.Waiting, "Dispute is not awaiting arbitration.");

        dispute.ruling = _ruling;
        dispute.status = DisputeStatus.Solved;

        msg.sender.send(arbitrationCost(""));
        dispute.arbitrated.rule(_disputeID, _ruling);
    }

First we check the caller address, we should only let the owner execute this. Then we do sanity checks: the ruling given by the arbitrator should be chosen among the choices and it should not be possible to rule on an already solved dispute. Afterwards, we update ruling and status values of the dispute. Then we pay arbitration fee to the arbitrator (owner). Finally, we call rule function of the arbitrated to enforce the ruling.

Lastly, appeal functions:

pragma solidity ^0.5;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {

    address public owner = msg.sender;

    struct Dispute {
        IArbitrable arbitrated;
        uint choices;
        uint ruling;
        DisputeStatus status;
    }

    Dispute[] public disputes;

    function arbitrationCost(bytes memory _extraData) public view returns(uint fee) {
        fee = 0.1 ether;
    }

    function appealCost(uint _disputeID, bytes memory _extraData) public view returns(uint fee) {
        fee = 2**250; // An unaffordable amount which practically avoids appeals.
    }

    function createDispute(uint _choices, bytes memory _extraData) public payable returns(uint disputeID) {
        require(msg.value >= arbitrationCost(_extraData), "Not enough ETH to cover arbitration costs.");

        disputeID = disputes.push(Dispute({
          arbitrated: IArbitrable(msg.sender),
          choices: _choices,
          ruling: uint(-1),
          status: DisputeStatus.Waiting
          })) -1;

        emit DisputeCreation(disputeID, IArbitrable(msg.sender));
    }

    function disputeStatus(uint _disputeID) public view returns(DisputeStatus status) {
        status = disputes[_disputeID].status;
    }

    function currentRuling(uint _disputeID) public view returns(uint ruling) {
        ruling = disputes[_disputeID].ruling;
    }

    function rule(uint _disputeID, uint _ruling) public {
        require(msg.sender == owner, "Only the owner of this contract can execute rule function.");

        Dispute storage dispute = disputes[_disputeID];

        require(_ruling <= dispute.choices, "Ruling out of bounds!");
        require(dispute.status == DisputeStatus.Waiting, "Dispute is not awaiting arbitration.");

        dispute.ruling = _ruling;
        dispute.status = DisputeStatus.Solved;

        msg.sender.send(arbitrationCost(""));
        dispute.arbitrated.rule(_disputeID, _ruling);
    }

    function appeal(uint _disputeID, bytes memory _extraData) public payable {
        require(msg.value >= appealCost(_disputeID, _extraData), "Not enough ETH to cover arbitration costs.");
    }

    function appealPeriod(uint _disputeID) public view returns(uint start, uint end) {
        return (0,0);
    }
}

Just a dummy implementation to conform the interface, as we don’t actually implement appeal functionality.

That’s it, we have a working, very simple centralized arbitrator!