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
andappeal
. Don’t forget to store the arbitrated contract and the disputeID (which should be unique). - Implement the functions for cost display (
arbitrationCost
andappealCost
). - 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!