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:

/**
 * @authors: [@ferittuncer, @hbarcelos]
 * @reviewers: []
 * @auditors: []
 * @bounties: []
    error InsufficientPayment(uint256 _available, uint256 _required);
    error InvalidRuling(uint256 _ruling, uint256 _numberOfChoices);
    error InvalidStatus(DisputeStatus _current, DisputeStatus _expected);

    struct Dispute {
        IArbitrable arbitrated;
        uint256 choices;
        uint256 ruling;
        DisputeStatus status;

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:

/**
 * @authors: [@ferittuncer, @hbarcelos]
 * @reviewers: []
 * @auditors: []
 * @bounties: []
 */
pragma solidity ^0.8.9;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {
    address public owner = msg.sender;

    error NotOwner();
    error InsufficientPayment(uint256 _available, uint256 _required);
    error InvalidRuling(uint256 _ruling, uint256 _numberOfChoices);
    error InvalidStatus(DisputeStatus _current, DisputeStatus _expected);

    struct Dispute {
        IArbitrable arbitrated;
        uint256 choices;
        uint256 ruling;
        DisputeStatus status;

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:

/**
 * @authors: [@ferittuncer, @hbarcelos]
 * @reviewers: []
 * @auditors: []
 * @bounties: []
 */
pragma solidity ^0.8.9;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {
    address public owner = msg.sender;

    error NotOwner();
    error InsufficientPayment(uint256 _available, uint256 _required);
    error InvalidRuling(uint256 _ruling, uint256 _numberOfChoices);
    error InvalidStatus(DisputeStatus _current, DisputeStatus _expected);

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

    Dispute[] public disputes;

    function arbitrationCost(bytes memory _extraData) public pure override returns (uint256) {
        return 0.1 ether;
    }

    function appealCost(uint256 _disputeID, bytes memory _extraData) public pure override returns (uint256) {
        return 2**250; // An unaffordable amount which practically avoids appeals.
    }

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:

/**
 * @authors: [@ferittuncer, @hbarcelos]
 * @reviewers: []
 * @auditors: []
 * @bounties: []
 */
pragma solidity ^0.8.9;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {
    address public owner = msg.sender;

    error NotOwner();
    error InsufficientPayment(uint256 _available, uint256 _required);
    error InvalidRuling(uint256 _ruling, uint256 _numberOfChoices);
    error InvalidStatus(DisputeStatus _current, DisputeStatus _expected);

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

    Dispute[] public disputes;

    function arbitrationCost(bytes memory _extraData) public pure override returns (uint256) {
        return 0.1 ether;
    }

    function appealCost(uint256 _disputeID, bytes memory _extraData) public pure override returns (uint256) {
        return 2**250; // An unaffordable amount which practically avoids appeals.
    }

    function createDispute(uint256 _choices, bytes memory _extraData)
        public
        payable
        override
        returns (uint256 disputeID)
    {
        uint256 requiredAmount = arbitrationCost(_extraData);
        if (msg.value < requiredAmount) {
            revert InsufficientPayment(msg.value, requiredAmount);
        }

        disputes.push(
            Dispute({arbitrated: IArbitrable(msg.sender), choices: _choices, ruling: 0, status: DisputeStatus.Waiting})

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:

/**
 * @authors: [@ferittuncer, @hbarcelos]
 * @reviewers: []
 * @auditors: []
 * @bounties: []
 * @deployments: []
 * SPDX-License-Identifier: MIT
 */
pragma solidity ^0.8.9;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {
    address public owner = msg.sender;

    error NotOwner();
    error InsufficientPayment(uint256 _available, uint256 _required);
    error InvalidRuling(uint256 _ruling, uint256 _numberOfChoices);
    error InvalidStatus(DisputeStatus _current, DisputeStatus _expected);

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

    Dispute[] public disputes;

    function arbitrationCost(bytes memory _extraData) public pure override returns (uint256) {
        return 0.1 ether;
    }

    function appealCost(uint256 _disputeID, bytes memory _extraData) public pure override returns (uint256) {
        return 2**250; // An unaffordable amount which practically avoids appeals.
    }

    function createDispute(uint256 _choices, bytes memory _extraData)
        public
        payable
        override
        returns (uint256 disputeID)
    {
        uint256 requiredAmount = arbitrationCost(_extraData);
        if (msg.value < requiredAmount) {

Then the proxy function:

/**
 * @authors: [@ferittuncer, @hbarcelos]
 * @reviewers: []
 * @auditors: []
 * @bounties: []
 * @deployments: []
 * SPDX-License-Identifier: MIT
 */
pragma solidity ^0.8.9;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {
    address public owner = msg.sender;

    error NotOwner();
    error InsufficientPayment(uint256 _available, uint256 _required);
    error InvalidRuling(uint256 _ruling, uint256 _numberOfChoices);
    error InvalidStatus(DisputeStatus _current, DisputeStatus _expected);

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

    Dispute[] public disputes;

    function arbitrationCost(bytes memory _extraData) public pure override returns (uint256) {
        return 0.1 ether;
    }

    function appealCost(uint256 _disputeID, bytes memory _extraData) public pure override returns (uint256) {
        return 2**250; // An unaffordable amount which practically avoids appeals.
    }

    function createDispute(uint256 _choices, bytes memory _extraData)
        public
        payable
        override
        returns (uint256 disputeID)
    {
        uint256 requiredAmount = arbitrationCost(_extraData);
        if (msg.value < requiredAmount) {
            revert InsufficientPayment(msg.value, requiredAmount);
        }

        disputes.push(
            Dispute({arbitrated: IArbitrable(msg.sender), choices: _choices, ruling: 0, status: DisputeStatus.Waiting})
        );

        disputeID = disputes.length - 1;
        emit DisputeCreation(disputeID, IArbitrable(msg.sender));
    }

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

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:

/**
 * @authors: [@ferittuncer, @hbarcelos]
 * @reviewers: []
 * @auditors: []
 * @bounties: []
 * @deployments: []
 * SPDX-License-Identifier: MIT
 */
pragma solidity ^0.8.9;

import "../IArbitrator.sol";

contract SimpleCentralizedArbitrator is IArbitrator {
    address public owner = msg.sender;

    error NotOwner();
    error InsufficientPayment(uint256 _available, uint256 _required);
    error InvalidRuling(uint256 _ruling, uint256 _numberOfChoices);
    error InvalidStatus(DisputeStatus _current, DisputeStatus _expected);

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

    Dispute[] public disputes;

    function arbitrationCost(bytes memory _extraData) public pure override returns (uint256) {
        return 0.1 ether;
    }

    function appealCost(uint256 _disputeID, bytes memory _extraData) public pure override returns (uint256) {
        return 2**250; // An unaffordable amount which practically avoids appeals.
    }

    function createDispute(uint256 _choices, bytes memory _extraData)
        public
        payable
        override
        returns (uint256 disputeID)
    {
        uint256 requiredAmount = arbitrationCost(_extraData);
        if (msg.value < requiredAmount) {
            revert InsufficientPayment(msg.value, requiredAmount);
        }

        disputes.push(
            Dispute({arbitrated: IArbitrable(msg.sender), choices: _choices, ruling: 0, status: DisputeStatus.Waiting})
        );

        disputeID = disputes.length - 1;
        emit DisputeCreation(disputeID, IArbitrable(msg.sender));
    }

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

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

    function rule(uint256 _disputeID, uint256 _ruling) public {
        if (msg.sender != owner) {
            revert NotOwner();
        }

        Dispute storage dispute = disputes[_disputeID];

        if (_ruling > dispute.choices) {
            revert InvalidRuling(_ruling, dispute.choices);
        }
        if (dispute.status != DisputeStatus.Waiting) {
            revert InvalidStatus(dispute.status, DisputeStatus.Waiting);
        }

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

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

    function appeal(uint256 _disputeID, bytes memory _extraData) public payable override {
        uint256 requiredAmount = appealCost(_disputeID, _extraData);
        if (msg.value < requiredAmount) {
            revert InsufficientPayment(msg.value, requiredAmount);
        }
    }

    function appealPeriod(uint256 _disputeID) public pure override returns (uint256 start, uint256 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!