Getting Started

This is a tutorial which will demonstrate how easy it is to create a disputable contract using the celeste-helpers package

Developing smart contracts integrating with Celeste

This tutorial assumes you already have node.js, npm and truffle installed. The setup commands are shown for a bash terminal. You may need to adapt some commands for other OS and terminals.

If you already have a truffle project configured (solidity compiler version at a minimum of 0.8.x) you can jump straight to step 3.

1. Initializing your project

Enter the following commands one by one into your terminal:

mkdir my-celeste-project // creates your project directory
cd my-celeste-project // moves to new directly
npm init -y
truffle init .

This will initialize a new npm/truffle project.

2. Configuring truffle

Open your truffle-config.js file which should've been generated as the truffle project was initialized and make sure you set the compiler to be at least solidity 0.8.x . An example config (the default truffle comments have been removed but you may keep as they may prove to be useful):

module.exports = {
  networks: {  },
  mocha: {  },
  compilers: {
    solc: {
      version: '0.8.4',
      settings: {
        optimizer: {
          enabled: true,
          runs: 10000
        }
      }
    }
  }
}

3. Installing celeste-helpers

Going back to your terminal you can easily install the package via npm:

npm install celeste-helpers

Since one of celeste-helpers dependencies is @openzeppelin/contracts you do not need to install it in case you were planning on using the library.

4. Creating a Celeste disputable contract

Now that all the necessary parts are installed you can start writing your contract.

4.1 Disputable contract boilerplate

We'll create a simple employment agreement so we'll name our contract "WorkAgreement". Our contract inherits from the Disputable parent contract provided by celeste-helpers to facilitate creating a dispute from within our contract.

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;

import "celeste-helpers/contracts/Disputable.sol";

contract WorkAgreement is Disputable {
    constructor(address _arbitrator, address _arbitratorManifest)
        Disputable(_arbitrator, _arbitratorManifest) payable
    { }
}

We make our constructor payable as we want the agreement to hold some funds. To simplify the contract a bit it will only be holding xDai.

4.2 Payment release

To make the contract easy to use and not always requiring a dispute we'll make it optimistic. What this means is that we'll program the contract so that it assumes everything is going well and that the underlying subjective agreement is not being violated. Only if something goes wrong will we ask Celeste to settle the dispute.

In order to leave time for initiating a potential dispute and allow the contractor or employee to fulfil their task we'll allow for a releaseAt parameter which will be the timestamp at which the contract allows the release of funds.

This is a simple contract so instead of transferring the funds directly to the recipient we can destroy the contract pointing it to the recipient. This is useful as the selfdestruct operation not only deletes the current contract but also transfers all remaining funds to the address it's pointed to. It also cannot revert unlike a normal transfer.

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;

import "celeste-helpers/contracts/Disputable.sol";

contract WorkAgreement is Disputable {
    uint256 public immutable releastAt;

    constructor(
        address _arbitrator,
        address _arbitratorManifest,
        uint256 _releaseAt
    )
        Disputable(_arbitrator, _arbitratorManifest) payable
    {
        releaseAt = _releaseAt;
    }

    function releasePayment() external {
        require(block.timestamp >= releaseAt, "WorkAgreement: not yet unlocked");
        selfdestruct(payable(msg.sender));
    }
}

However, a problem with this code is that anyone can call the releasePayment method once the releaseAt timestamp has passed. To prevent someone stealing the funds we'll access restrict the method by introducing a new parameter which will store the recipient, we'll call the variable contractor:

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;

import "celeste-helpers/contracts/Disputable.sol";

contract WorkAgreement is Disputable {
    uint256 public immutable releaseAt;
    address public immutable contractor;

    constructor(
        address _arbitrator,
        address _arbitratorManifest,
        uint256 _releaseAt,
        address _contractor
    )
        Disputable(_arbitrator, _arbitratorManifest) payable
    {
        releaseAt = _releaseAt;
        contractor = _contractor;
    }
    
    function releasePayment() external {
        require(msg.sender == contractor, "WorkAgreement: not contractor");
        require(block.timestamp >= releaseAt, "WorkAgreement: not yet unlocked");
        selfdestruct(payable(msg.sender));
    }
}

4.3 Making the payment disputable

Let's create a new method dispute that allows a dispute to be created with Celeste. There's a few things that have to be considered when creating a dispute:

  1. Who are the conflicting parties (defendant and challenger)?

  2. How will the Celeste dispute creation be payed?

  3. What's the agreement metadata and where will it be stored?

  4. What will your contract do or not do while it is waiting for a ruling?

Considering these aspects for our contract:

  1. It's quite straight forward here: the employer is the one who'd dispute the release of a payment and would thus be the challenger. The contract is the defendant.

  2. We can think of several mechanisms to fairly balance the fee payment for our dispute but we'll say that it's up to the employer to pay the Celeste fee if he wants to dispute the payment.

  3. We'll simply store the agreement data in the contract. To ensure that the employer or contractor can't just change the agreement as they please we'll set it once in the constructor.

  4. While the dispute is being arbitrated in Celeste we'll just prevent the releasePayment from being called:

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;

import "celeste-helpers/contracts/Disputable.sol";

contract WorkAgreement is Disputable {
    uint256 public immutable releastAt;
    address public immutable employer;
    address public immutable contractor;
    bytes public agreementMetadata;

    bool public beingDisputed;

    constructor(
        address _arbitrator,
        address _arbitratorManifest,
        bytes memory _agreementMetadata,
        uint256 _releaseAt,
        address _contractor
    )
        Disputable(_arbitrator, _arbitratorManifest) payable
    {
        agreementMetadata = _agreementMetadata;
        releaseAt = _releaseAt;
        employer = msg.sender;
        contractor = _contractor;
    }

    function releasePayment() external {
        require(!beingDisputed, "WorkAgreement: being disputed");
        require(msg.sender == contractor, "WorkAgreement: not contractor");
        require(block.timestamp >= releaseAt, "WorkAgreement: not yet unlocked");
        selfdestruct(payable(msg.sender));
    }

    function dispute() external {
        require(!beingDisputed, "WorkAgreement: being disputed");
        require(msg.sender == employer, "WorkAgreement: not employer");
        beingDisputed = true;
        _prepareAndPullDisputeFeeFrom(msg.sender);
        _createDisputeAgainst(contractor, msg.sender, agreementMetadata);
    }
}

The _prepareAndPullDisputeFeeFrom pulls the fee from the specified address if it has the necessary allowance and makes sure the Celeste fee can be paid. The _createDisputeAgainst method then creates the dispute with Celeste.

4.4 Handling the Celeste ruling

Now that our payment is disputable we also want our contract to be able to settle the dispute. In order to do that we also need to store the dispute ID of the dispute we create. We can easily get it as it's returned by _createDisputeAgainst:

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;

import "celeste-helpers/contracts/Disputable.sol";

contract WorkAgreement is Disputable {
    uint256 public immutable releaseAt;
    address public immutable employer;
    address public immutable contractor;
    bytes public agreementMetadata;

    bool public beingDisputed;
    uint256 public disputeId;

    constructor(
        address _arbitrator,
        address _arbitratorManifest,
        bytes memory _agreementMetadata,
        uint256 _releaseAt,
        address _contractor
    )
        Disputable(_arbitrator, _arbitratorManifest) payable
    {
        agreementMetadata = _agreementMetadata;
        releaseAt = _releaseAt;
        employer = msg.sender;
        contractor = _contractor;
    }

    function releasePayment() external {
        require(!beingDisputed, "WorkAgreement: being disputed");
        require(msg.sender == contractor, "WorkAgreement: not contractor");
        require(block.timestamp >= releaseAt, "WorkAgreement: not yet unlocked");
        selfdestruct(payable(msg.sender));
    }

    function dispute() external {
        require(!beingDisputed, "WorkAgreement: being disputed");
        require(msg.sender == employer, "WorkAgreement: not employer");
        beingDisputed = true;
        _prepareAndPullDisputeFeeFrom(msg.sender);
        disputeId = _createDisputeAgainst(contractor, msg.sender, agreementMetadata);
    }

    function settleDispute() external {
        require(beingDisputed, "WorkAgreement: Not being disputed");
        uint256 ruling = _getRulingOf(disputeId);
        selfdestruct(payable(ruling == RULING_AGAINST_ACTION ? employer : contractor));
    }
}

If there is no ruling yet the arbitrator.rule method will simply revert. The ruling can be one of three states (RULING_REFUSED, RULING_AGAINST_ACTION, RULING_FOR_ACTION). To make our contract simpler we'll give the contractor the benefit of the doubt, so unless Celeste rules explicitly against the release of the payment the contractor will receive the money. However if the court rules against the action the funds in the contract will be returned to the employer.

4.5 Hiding the agreement

While not necessary we can make a small enhancement to our agreement, we can hide the specifics of the agreement. Since the agreement metadata is a memory variable it can be quite gas intensive to write and read from storage. Furthermore the contract doesn't need to know the entire metadata unless a dispute is initiated. If it comes to a dispute the agreement will have to be revealed publicly on the blockchain in order to allow Celeste jurors to rule based on its contents and evidence but initially hiding the contract can give some privacy to the two parties if they are not in a dispute.

In order to hide the contents of the agreement while still have it be immutable and binding we can use the power of cryptographic commitments. While this may sound complicated the concept of commitments is really quite simple. All we do is hash the agreement along with an additional random piece of data (the salt), the resulting hash aka the "commitment" is then submitted to our contract instead of the entire agreement. This is more efficient as regardless of the size of the agreement metadata our contract will only need to store one 32-byte value.

commitment=keccak(agreementsalt) commitment = keccak(agreement || salt)

The hash function ensures that nobody can reverse the commitment to retrieve the commitment. The random salt ensures that the same agreement can be reused without the resulting commitment being identical. Thankfully solidity allows us to easily use such cryptography directly in our contracts:

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;

import "celeste-helpers/contracts/Disputable.sol";

contract WorkAgreement is Disputable {
    uint256 public immutable releaseAt;
    address public immutable employer;
    address public immutable contractor;
    bytes32 public immutable agreementCommitment;
    bool public beingDisputed;
    uint256 public disputeId;

    constructor(
        address _arbitrator,
        address _arbitratorManifest,
        bytes32 _agreementCommitment,
        uint256 _releaseAt,
        address _contractor
    )
        Disputable(_arbitrator, _arbitratorManifest) payable
    {
        agreementCommitment = _agreementCommitment;
        releaseAt = _releaseAt;
        employer = msg.sender;
        contractor = _contractor;
    }

    function releasePayment() external {
        require(!beingDisputed, "WorkAgreement: being disputed");
        require(msg.sender == contractor, "WorkAgreement: not contractor");
        require(block.timestamp >= releaseAt, "WorkAgreement: not yet unlocked");
        selfdestruct(payable(msg.sender));
    }

    function dispute(bytes32 _salt, bytes calldata _agreementMetadata) external {
        require(!beingDisputed, "WorkAgreement: being disputed");
        require(msg.sender == employer, "WorkAgreement: not employer");
        require(
            agreementCommitment == keccak256(abi.encode(_salt, _agreementMetadata)),
            "WorkAgreement: invalid agreement"
        );
        beingDisputed = true;
        _prepareAndPullDisputeFeeFrom(msg.sender);
        disputeId = _createDisputeAgainst(contractor, msg.sender, _agreementMetadata);
    }

    function settleDispute() external {
        require(beingDisputed, "WorkAgreement: Not being disputed");
        uint256 ruling = _getRulingOf(disputeId);
        selfdestruct(payable(ruling == RULING_AGAINST_ACTION ? employer : contractor));
    }
}

Once the contract is deployed the employer can give the salt along with the agreement to the contractor to allow him to verify that the contract's agreement is what he expects it to be. If the employer wants to dispute the payment he just needs to submit the agreement along with the salt to the contract.

4.6 Further improvements

Further improvements could be made like allowing the agreement to be changed if both parties agree, allowing the contractor to directly return the payment to employer if they want to, using ERC20 tokens for payment, streaming the payment, breaking up the payment into smaller parts fixed to certain goals etc.

5. Testing your contracts

The celeste-helpers package also provides mocks of Celeste to allow you to easily test your contracts in a local environment without needing to redeploy and configure an entire Celeste instance.

Testing will be covered in the Testing disputable contracts section.

6. Using the court manifest and submitting evidence

By inheriting from the Disputable parent contract your contract automatically enables relevant participants and their representatives to submit evidence for disputes created using the _createDisputeAgainst method.

Final Notes

The combination of the immutability of smart contracts and the decentralized subjectivity enabled by Celeste make smart contracts much more useful than they would be on their own.

For a full documentation of the features provided by the celeste-helpers package and how to use them feel free to refer to the Disputable API and Testing disputable contracts sections.

Last updated