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:
- No dispute arises,
payee
withdraws the funds. payer
reclaims funds by depositing arbitration fee…payee
fails to deposit arbitration fee inarbitrationFeeDepositPeriod
andpayer
wins by default. The arbitration fee deposit paid bypayer
refunded.payee
deposits arbitration fee in time. Dispute gets created.arbitrator
rules. Winner gets the arbitration fee refunded.
- No dispute arises,
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:
/**
* @authors: [@ferittuncer, @hbarcelos]
* @reviewers: []
* @auditors: []
* @bounties: []
* @deployments: []
* SPDX-License-Identifier: MIT
*/
pragma solidity ^0.8.9;
import "../IArbitrable.sol";
import "../IArbitrator.sol";
contract SimpleEscrow is IArbitrable {
error InvalidStatus();
error ReleasedTooEarly();
error NotPayer();
error NotArbitrator();
error PayeeDepositStillPending();
error ReclaimedTooLate();
error InsufficientPayment(uint256 _available, uint256 _required);
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:
/**
* @authors: [@ferittuncer, @hbarcelos]
* @reviewers: []
* @auditors: []
* @bounties: []
* @deployments: []
* SPDX-License-Identifier: MIT
*/
pragma solidity ^0.8.9;
import "../IArbitrable.sol";
import "../IArbitrator.sol";
contract SimpleEscrow is IArbitrable {
address payable public payer = payable(msg.sender);
address payable public payee;
uint256 public value;
IArbitrator public arbitrator;
string public agreement;
error InvalidStatus();
error ReleasedTooEarly();
error NotPayer();
error NotArbitrator();
error PayeeDepositStillPending();
error ReclaimedTooLate();
error InsufficientPayment(uint256 _available, uint256 _required);
error InvalidRuling(uint256 _ruling, uint256 _numberOfChoices);
enum Status {
Initial,
Reclaimed,
Disputed,
Resolved
}
Status public status;
uint256 public reclaimedAt;
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:
/**
* @authors: [@ferittuncer, @hbarcelos]
* @reviewers: []
* @auditors: []
* @bounties: []
* @deployments: []
* SPDX-License-Identifier: MIT
*/
pragma solidity ^0.8.9;
import "../IArbitrable.sol";
import "../IArbitrator.sol";
contract SimpleEscrow is IArbitrable {
address payable public payer = payable(msg.sender);
address payable public payee;
uint256 public value;
IArbitrator public arbitrator;
string public agreement;
uint256 public createdAt;
uint256 public constant reclamationPeriod = 3 minutes; // Timeframe is short on purpose to be able to test it quickly. Not for production use.
uint256 public constant arbitrationFeeDepositPeriod = 3 minutes; // Timeframe is short on purpose to be able to test it quickly. Not for production use.
error InvalidStatus();
error ReleasedTooEarly();
error NotPayer();
error NotArbitrator();
error PayeeDepositStillPending();
error ReclaimedTooLate();
error InsufficientPayment(uint256 _available, uint256 _required);
error InvalidRuling(uint256 _ruling, uint256 _numberOfChoices);
enum Status {
Initial,
Reclaimed,
Disputed,
Resolved
}
Status public status;
uint256 public reclaimedAt;
enum RulingOptions {
RefusedToArbitrate,
PayerWins,
PayeeWins
}
uint256 constant numberOfRulingOptions = 2; // Notice that option 0 is reserved for RefusedToArbitrate.
constructor(
address payable _payee,
IArbitrator _arbitrator,
string memory _agreement
) payable {
value = msg.value;
payee = _payee;
arbitrator = _arbitrator;
agreement = _agreement;
createdAt = block.timestamp;
}
function releaseFunds() public {
if (status != Status.Initial) {
revert InvalidStatus();
}
if (msg.sender != payer && block.timestamp - createdAt <= reclamationPeriod) {
revert ReleasedTooEarly();
}
status = Status.Resolved;
payee.send(value);
}
function reclaimFunds() public payable {
if (status != Status.Initial && status != Status.Reclaimed) {
revert InvalidStatus();
}
if (msg.sender != payer) {
revert NotPayer();
}
if (status == Status.Reclaimed) {
if (block.timestamp - reclaimedAt <= arbitrationFeeDepositPeriod) {
revert PayeeDepositStillPending();
}
payer.send(address(this).balance);
status = Status.Resolved;
} else {
if (block.timestamp - createdAt > reclamationPeriod) {
revert ReclaimedTooLate();
}
uint256 requiredAmount = arbitrator.arbitrationCost("");
if (msg.value < requiredAmount) {
revert InsufficientPayment(msg.value, requiredAmount);
}
reclaimedAt = block.timestamp;
status = Status.Reclaimed;
}
}
function depositArbitrationFeeForPayee() public payable {
if (status != Status.Reclaimed) {
revert InvalidStatus();
}
arbitrator.createDispute{value: msg.value}(numberOfRulingOptions, "");
status = Status.Disputed;
}
function rule(uint256 _disputeID, uint256 _ruling) public override {
if (msg.sender != address(arbitrator)) {
revert NotArbitrator();
}
if (status != Status.Disputed) {
revert InvalidStatus();
}
if (_ruling > numberOfRulingOptions) {
revert InvalidRuling(_ruling, numberOfRulingOptions);
}
status = Status.Resolved;
if (_ruling == uint256(RulingOptions.PayerWins)) payer.send(address(this).balance);
else if (_ruling == uint256(RulingOptions.PayeeWins)) payee.send(address(this).balance);
emit Ruling(arbitrator, _disputeID, _ruling);
}
function remainingTimeToReclaim() public view returns (uint256) {
if (status != Status.Initial) {
revert InvalidStatus();
}
return
(block.timestamp - createdAt) > reclamationPeriod ? 0 : (createdAt + reclamationPeriod - block.timestamp);
}
function remainingTimeToDepositArbitrationFee() public view returns (uint256) {
if (status != Status.Reclaimed) {
revert InvalidStatus();
}
return
(block.timestamp - reclaimedAt) > arbitrationFeeDepositPeriod
? 0
: (reclaimedAt + arbitrationFeeDepositPeriod - block.timestamp);
}
}
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.