Implementing a Complex Arbitrable

Warning

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

Let’s implement a full-fledged escrow this time, extending SimpleEscrowWithERC1497 contract we implemented earlier. We will call it just Escrow this time.

Recall SimpleEscrowWithERC1497:

pragma solidity ^0.5;

import "../IArbitrable.sol";
import "../IArbitrator.sol";
import "../erc-1497/IEvidence.sol";

contract SimpleEscrowWithERC1497 is IArbitrable, IEvidence {
    address payable public payer = msg.sender;
    address payable public payee;
    uint public value;
    IArbitrator public arbitrator;
    uint constant public reclamationPeriod = 3 minutes;
    uint constant public arbitrationFeeDepositPeriod = 3 minutes;

    uint public createdAt;
    uint public reclaimedAt;

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

    enum RulingOptions {RefusedToArbitrate, PayerWins, PayeeWins}
    uint constant numberOfRulingOptions = 2;

    uint constant metaevidenceID = 0;
    uint constant evidenceGroupID = 0;

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

        emit MetaEvidence(metaevidenceID, _metaevidence);
    }

    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.");
        uint disputeID = arbitrator.createDispute.value(msg.value)(numberOfRulingOptions, "");
        status = Status.Disputed;
        emit Dispute(arbitrator, disputeID, metaevidenceID, evidenceGroupID);
    }

    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 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);
    }

    function submitEvidence(string memory _evidence) public {
        require(status != Status.Resolved, "Transaction is not in Resolve state.");
        require(msg.sender == payer || msg.sender == payee, "Third parties are not allowed to submit evidence.");
        emit Evidence(arbitrator, evidenceGroupID, msg.sender, _evidence);
    }
}

The payer needs to deploy a contract for each transaction, but contract deployment is expensive. Instead, we could use the same contract for multiple transactions between arbitrary parties with arbitrary arbitrators.

Let’s separate contract deployment and transaction creation:

pragma solidity ^0.5;

import "../IArbitrable.sol";
import "../IArbitrator.sol";
import "../erc-1497/IEvidence.sol";

contract Escrow is IArbitrable, IEvidence {

    enum Status {Initial, Reclaimed, Disputed, Resolved}
    enum RulingOptions {RefusedToArbitrate, PayerWins, PayeeWins}
    uint constant numberOfRulingOptions = 2;

    constructor() public {
    }

    struct TX {
        address payable payer;
        address payable payee;
        IArbitrator arbitrator;
        Status status;
        uint value;
        uint disputeID;
        uint createdAt;
        uint reclaimedAt;
        uint payerFeeDeposit;
        uint payeeFeeDeposit;
        uint reclamationPeriod;
        uint arbitrationFeeDepositPeriod;
    }

    TX[] public txs;
    mapping (uint => uint) disputeIDtoTXID;

    function newTransaction(address payable _payee, IArbitrator _arbitrator, string memory _metaevidence, uint _reclamationPeriod, uint _arbitrationFeeDepositPeriod) public payable returns (uint txID){
        emit MetaEvidence(txs.length, _metaevidence);

        return txs.push(TX({
            payer: msg.sender,
            payee: _payee,
            arbitrator: _arbitrator,
            status: Status.Initial,
            value: msg.value,
            disputeID: 0,
            createdAt: now,
            reclaimedAt: 0,
            payerFeeDeposit: 0,
            payeeFeeDeposit: 0,
            reclamationPeriod: _reclamationPeriod,
            arbitrationFeeDepositPeriod: _arbitrationFeeDepositPeriod
          })) -1;
    }

    function releaseFunds(uint _txID) public {
        TX storage tx = txs[_txID];

        require(tx.status == Status.Initial, "Transaction is not in Initial state.");
        if (msg.sender != tx.payer)
          require(now - tx.createdAt > tx.reclamationPeriod, "Payer still has time to reclaim.");

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

    function reclaimFunds(uint _txID) public payable {
        TX storage tx = txs[_txID];

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

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

    function depositArbitrationFeeForPayee(uint _txID) public payable {
        TX storage tx = txs[_txID];

        require(tx.status == Status.Reclaimed, "Transaction is not in Reclaimed state.");

        tx.payeeFeeDeposit = msg.value;
        tx.disputeID = tx.arbitrator.createDispute.value(msg.value)(numberOfRulingOptions, "");
        tx.status = Status.Disputed;
        disputeIDtoTXID[tx.disputeID] = _txID;
        emit Dispute(tx.arbitrator, tx.disputeID, _txID, _txID);
    }

    function rule(uint _disputeID, uint _ruling) public {
        uint txID = disputeIDtoTXID[_disputeID];
        TX storage tx = txs[txID];

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

        tx.status = Status.Resolved;

        if (_ruling == uint(RulingOptions.PayerWins)) tx.payer.send(tx.value + tx.payerFeeDeposit);
        else tx.payee.send(tx.value + tx.payeeFeeDeposit);
        emit Ruling(tx.arbitrator, _disputeID, _ruling);
    }


    function submitEvidence(uint _txID, string memory _evidence) public {
        TX storage tx = txs[_txID];

        require(tx.status != Status.Resolved);
        require(msg.sender == tx.payer || msg.sender == tx.payee, "Third parties are not allowed to submit evidence.");

        emit Evidence(tx.arbitrator, _txID, msg.sender, _evidence);
    }

    function remainingTimeToReclaim(uint _txID) public view returns (uint) {
        TX storage tx = txs[_txID];

        if (tx.status != Status.Initial) revert("Transaction is not in Initial state.");
        return (tx.createdAt + tx.reclamationPeriod - now) > tx.reclamationPeriod ? 0 : (tx.createdAt + tx.reclamationPeriod - now);
    }

    function remainingTimeToDepositArbitrationFee(uint _txID) public view returns (uint) {
        TX storage tx = txs[_txID];

        if (tx.status != Status.Reclaimed) revert("Transaction is not in Reclaimed state.");
        return (tx.reclaimedAt + tx.arbitrationFeeDepositPeriod - now) > tx.arbitrationFeeDepositPeriod ? 0 : (tx.reclaimedAt + tx.arbitrationFeeDepositPeriod - now);
    }

}

We first start by removing the global state variables and defining TX struct. Each instance of this struct will represent a transaction, thus will have transaction-specific variables instead of globals. We stored transactions inside txs array. We also created new transactions via newTransaction function.

newTransaction function simply takes transaction-specific information and pushes a TX into txs. This txs array is append-only, we will never remove any item. By implementing this, we can uniquely identify each transaction by their index in the array.

Next, we updated all the functions with transaction-specific variables instead of globals. Changes are merely adding tx. prefixes in front of expressions.

We also stored fee deposits for each party, as the smart contract now has balances for multiple transactions that we can’t send(address(this).balance). Instead, we used tx.payer.send(tx.value + tx.payerFeeDeposit); if payer wins and tx.payee.send(tx.value + tx.payeeFeeDeposit); if payee wins.

Notice that rule function has no transaction ID parameter, but we need to obtain transaction details of given dispute. We achieved this by storing the transaction ID for respective dispute ID as disputeIDtoTXID. Just after dispute creation (inside depositArbitrationFeeForPayee), we store this relation with disputeIDtoTXID[tx.disputeID] = _txID; statement.

Good job! Now we have an escrow contract which can handle multiple transactions between different parties and arbitrators.