Introducing Olive Oil Trust: smart contracts

View on GitHub

This post is part of a series of posts regarding Olive Oil Trust:

Olive Oil Trust smart contracts are implemented in order to adopt a set of rules so that all members of the supply chain that have the same roles are established rigorously within the same framework.

In this post I will elaborate on how I designed, wrote, tested and deployed these contracts.

In this article

Designing the contracts

In order for an account in Ethereum to become a member in Olive Oil Trust with a specific role, it has to deploy a contract that inherits from that role contract (the contracts are written in Solidity language).

Members have ownership of their own tokens and escrows (or certificates in the case of certifiers) in order to perform their actions and set states accordingly.

Let's say we are a bottling plant named Bottling Company and we'd like to adopt this role in Olive Oil Trust. We'd have to deploy the following contract:

hardhat-env/contracts/members/BottlingCompany.sol

As we can see this would require the deployment of two token contracts and an escrow contract first in order to pass their addresses as arguments and initialise the contract.

Furthermore, Olive Oil Trust contracts are designed for use in upgradeable contracts, as we can see in the code block above since the contract inherits from OpenZeppelin's Initializable.sol and there is an initialize function instead of a constructor.

Olive Oil Trust uses upgradeable contracts to give the possibility to their members to upgrade their contracts to a newer version.

When deploying an upgradeable contract with OpenZeppelin upgrade plugins, a proxy is deployed which cannot be altered.

Instead, the implementation contract to which the transactions are forwarded would be replaced for a new one if we were to upgrade the contracts.

Please, visit this blog post from OpenZeppelin for more information.

Most of the contracts follow an UUPS proxy pattern; however, due to size limits, some others follow a transparent proxy pattern instead.

The contracts that conform Olive Oil Trust are shown below:

Roles

There are seven roles in Olive Oil Trust: olive growers, olive oil mills, bottle manufacturers, bottling plants, distributors, retailers and certifiers.

You can find them all in hardhat-env/contracts/OliveOilTrust/roles.

They inherit from base contracts as well as OpenZeppelin's utils and access upgradeable contracts.

For example, let's focus on the contract for the bottling plant role and go over its main aspects and non-private functions.

hardhat-env/contracts/OliveOilTrust/roles/BottlingPlantUpgradeable.sol

As we can see this contract inherits from six contracts:

  • @openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol: this is a base contract that aids us in writing upgradeable contracts with the use of modifiers.

  • @openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol: an upgradeability mechanism designed for UUPS proxy. To properly use it, the _authorizeUpgrade function must be overridden to include access restriction to the upgrade mechanism, as we can see in line 53.

  • hardhat-env/contracts/OliveOilTrust/base/DependentCreator.sol: this contracts gathers functions regarding the mintage of dependent tokens, which we'll see later in this post.

  • hardhat-env/contracts/OliveOilTrust/base/BaseSeller.sol: base contract that implements actions taken by a seller in the value chain. It interacts with the member's owned tokens and escrow contracts.

  • @openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol: it allows the contract to hold ERC1155 tokens.

  • @openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol: it provides an access control mechanism.

__BottlingPlantUpgradeable_init gathers linearized calls to parent initializers whereas __BottlingPlantUpgradeable_init_unchained is equivalent to the initializer function minus the calls to parent initializers and gathers the logic that would be in a constructor function.

Initializer functions are not linearized by the compiler, like constructors are. This mechanism, which I have replicated from OpenZeppelin's upgradeable contracts, is used to avoid initialising the same contract twice.

The contract stores the addresses of industrialUnitToken_ and escrow_ in state variables and grants permissions for:

  • escrow_ to operate with the bottling plant's tokens in industrialUnitToken_. The escrow must be be able to transfer the bottling plant's tokens to itself when depositing them.

  • industrialUnitToken_ to operate with the bottling plant's tokens in dependentToken. The industrial unit token contract must be able to transfer the bottling plant's tokens to itself when packing them.

Let's now look at the functions inherited from DependentCreator.sol (since the rest of the functions merely call respective functions of tokens and escrows and rely on their implementation which we'll see later).

These functions, which rely entirely on their implementation in DependentCreator.sol, are:

  • setTokenTypeInstructions and setTokenTypesInstructions: these functions set one set, or multiple sets, of instructions so that a DependentCreator member can validate the mintage of a new token of that type. Both functions rely entirely on its implementation in hardhat-env/contracts/OliveOilTrust/tokens/DependentTokenUpgradeable.sol which we'll see later in tokens.
hardhat-env/contracts/OliveOilTrust/roles/DependentCreator.sol
  • mint and mintBatch: they mint one batch or multiple batches of a type of token.

Although, it might confuse that mint mints one batch of a token and mintBatch mints several batches of a type of token, this is to go on with the nomenclature used by OpenZeppelin in the implementation of the ERC-1155 multi token standard.

hardhat-env/contracts/OliveOilTrust/roles/DependentCreator.sol

For every batch that is going to be minted, these functions validate that we have enough units of the tokens that are required to mint the tokens we need.

Validation.validate fulfills the following purposes:

  1. It performs security checks to ensure all the dimensions of the arrays passed are valid and reverts otherwise.

  2. It gets the instructions of the type of token that is to be validated and ensures that the tokens being used to mint the dependent token comply with these instructions.

  3. If the aforementioned steps are successful, it consumes the instructed amount of every input token used in the process.

We can see the implementation below:

hardhat-env/contracts/OliveOilTrust/libraries/Validation.sol

If the address/es of the token/s to be consumed does/do not match the address/es set in the instructions, it is assumed that these instructions use a certificate/s for that type of token.

Then, the function validate consults that certificate/s if the address/es of the token/s to be consumed is/are certified.

Once the parameters of the token/s to be minted are validated, an event or multiple events of type TokenAncestrySet is/are emitted. Then, the batch/es of token/s is/are minted.

This event stores information about the token ancestry so that it can be easily traced to its origin.

Tokens

There are three types of tokens:

  1. Independent tokens: tokens that do not require other tokens to be minted.

  2. Dependent tokens: tokens that are the result of transforming other tokens, thus being dependent on their availability.

  3. Industrial unit tokens: tokens that wrap tokens that represent commercial units (olive oil bottle tokens).

In order to simplify the process and somehow set the origin from where to start tracing a product, olive growers and bottle manufacturers both mint independent tokens in Olive Oil Trust, i.e. original tokens.

The implementation of an independent token is simpler than that of a dependent token since there are no instructions to set or token mintage validation, so let's look at the code for dependent and industrial unit tokens.

Dependent tokens

The main functions in DependentTokenUpgradeable.sol are:

  • setTokenTypeInstructions and setTokenTypesInstructions: they check that the parameters passed are valid, that the id/s of the type/s of token/s are not duplicated and, furthermore, they set the instructions in a mapping state variable and emits an event or multiple events of type TokenTypeInstructionsSet so that the instructions can be indexed by the subgraph mappings.

    hardhat-env/contracts/OliveOilTrust/tokens/DependentTokenUpgradeable.sol
  • mint and mintBatch: they mint one or multiple batches of tokens. Tokens in a batch can either be non-fungible or fungible depending on if there is only one or multiple, respectively.

    All the units of the same batch are similar and, therefore, fungible. However, they are different from those of other batches even if they share the same type of token.

    hardhat-env/contracts/OliveOilTrust/tokens/DependentTokenUpgradeable.sol
  • getInstructions: this is a view function that returns the instructions of a given batch.

    hardhat-env/contracts/OliveOilTrust/tokens/DependentTokenUpgradeable.sol

Industrial unit tokens

One notable difference between independent and industrial unit tokens is that even though both implement ERC-1155 tokens, IndustrialUnitTokenUpgradeable.sol is enabled to hold ERC-1155 tokens (it inherits from OpenZeppelin's ERC1155HolderUpgradeable.sol).

In other words, the ownership of an ERC-1155 token can be transferred to the address of IndustrialUnitTokenUpgradeable.sol.

This is necessary to comply with the packing and unpacking logic that has to be implemented.

The main functions of the contract are:

  • pack and packBatch transfer ownership of the tokens to be packed from the bottling plant to the address of the industrial unit owned by the bottling plant. It emits an event of type SinglePacked or BatchPacked.

    hardhat-env/contracts/OliveOilTrust/tokens/IndustrialUnitTokenUpgradeable.sol
  • unpack and unpackBatch transfer ownership of the tokens to be packed from the address of the industrial unit owned by the bottling plant to the bottling plant. Then the industrial unit/s is/are burnt. It emits an event of type SingleUnpacked or BatchUnpacked.

    hardhat-env/contracts/OliveOilTrust/tokens/IndustrialUnitTokenUpgradeable.sol
  • The rest are three view functions to obtain information about the tokens.

    hardhat-env/contracts/OliveOilTrust/tokens/IndustrialUnitTokenUpgradeable.sol

Escrows

Escrows are the contracts that members use to trade tokens. Therefore, they are a mechanism to transfer the ownership of a token, or tokens, in exchange of an amount of ether but without the involvement of third parties.

There are three different escrows:

  • AgriculturalEscrowUpgradeable.sol: this escrow is meant to hold olive tokens in the agricultural phase of the olive oil value chain. There is a different escrow to hold this independent token because the price is set by the olive oil mill after checking the qualities of the olives that it is considering to buy. This is similar to making an offer to the olive grower for the ownership of their product.

  • CommercialUnitsEscrowUpgradeable.sol: this type of escrow is designed to hold the rest of the types of tokens but the industrial units. It is similar to the escrow described above but except the price is set by the seller.

  • IndustrialUnitsEscrowUpgradeable.sol: this escrow is meant to hold industrial unit tokens. The main difference with the other escrows is that it does not require an amount to be specified for every token deposited since all industrial units are minted as a single unit.

For instance, a bottling plant owns an escrow contract that inherits from IndustrialUnitsEscrowUpgradeable.sol since it sells industrial units to distributors.

This type of escrow also inherits from OpenZeppelin's contract ERC1155HolderUpgradeable.sol because the escrow will become owner of the tokens that are deposited until the escrow is closed:

hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol

The process of buying a token is represented in up to three of six different stages:

  1. A token/s is/are deposited, which sets the state of the escrow to Active.

    • The deposit of the token/s could now be reverted before a member deposits ether which would set the state to a final RevertedBeforePayment and would transfer the token/s back to the seller.
  2. The required amount of ether to buy the token/s is/are deposited, which sets the state to EtherDeposited.

    • The deposit of ether could now be cancelled by the buyer candidate, which would set the state back to Active and would transfer the funds back to the buyer.

    • The deposit of the token/s could also be reverted after ether is deposited, which would set the state to a final RevertedAfterPayment, transfer funds back to the buyer and token/s back to the seller.

  3. The escrow operation is completed and buyer and seller get token/s and ether respectively. The state of the escrow is finally set to Closed.

RevertedBeforePayment, RevertedAfterPayment and Closed are final states, meaning once these states are reached the escrow cannot be further modified, and neither will be holding ether nor tokens any longer.

Every single stage is reached by interacting with the escrow through a set of functions.

As we'll see below, there is a check at the beginning of every function (before any transfer) that makes it revert if the state to perform the called action is not the required one.

For instance, an escrow cannot be closed if funds have not been deposited.

Every single function emits an event of its type to store the required information of the escrow so that it can be indexed by the subgraph mappings.

Let's go over these functions:

  • depositToken and depositBatch: these functions deposit one or more tokens to the escrow. The functions perform security checks to prevent invalid information to be set to the state of the escrow. Lastly, the tokens are transferred to the escrow contract. This is possible because permissions were granted to that effect, as we saw earlier, in BottlingPlantUpgradeable-__BottlingPlantUpgradeable_init_unchained. They can only be called by the owner of the contract, i.e. the seller. Their implementation is shown below:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • revertBeforePayment: it sets the state of the escrow to RevertedBeforePayment and transfers the tokens back to the seller. No more actions have to be done since no funds have been deposited at this point in time. It can only be called by the owner of the contract, i.e. the seller. Check the implementation below:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • makePayment: it sets the state to EtherDeposited and transfers ether to the escrow, it only accepts the same amount of ether as the price set by the seller. We can see the implementation below:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • cancelPayment: it cancels the payment, i.e. it sets the state of the escrow back to Active and sends the funds back to the buyer candidate. Only the same address that deposited the funds can cancel that payment. Check the implementation below:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • revertAfterPayment: it sets the state to RevertedAfterPayment, transfers the funds back to the buyer candidate and the token/s back to the seller. It can only be called by the owner of the contract, i.e. the seller. We can see the implementation below:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • close: it sets the state to Closed, transfers the funds to the seller and the token/s to the buyer. It can only be called by the owner of the contract, i.e. the seller. Check the implementation below:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • escrow and state: these view functions return the fields of the struct MyIndustrialUnitsEscrow of a given escrow and the state of this escrow respectively. Check the implementation below:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol

Certificate

The certificate contract enables the certifier to attest to the validity of a type of token to a certain standard.

As we can see below, it inherits from UUPSUpgradeable (so it overrides _authorizeUpgrade) and it takes a base URI to initialise the contract:

hardhat-env/contracts/OliveOilTrust/certificates/CertificateUpgradeable.sol

CertificateUpgradeable.sol provides the owner of the contract, a certifier, with the function certifyToken to validate a type of token and certifyBatch to validate multiple types of tokens.

Both emit events to store information about the certification/s:

hardhat-env/contracts/OliveOilTrust/certificates/CertificateUpgradeable.sol

CertificateUpgradeable.sol also implements three view functions:

  • isCertified: it returns if a type of token is certified.

    hardhat-env/contracts/OliveOilTrust/certificates/CertificateUpgradeable.sol
  • certificatesOf and certificatesOfBatch: the former returns the certificates of a given type of token and the latter multiple types of tokens.

    hardhat-env/contracts/OliveOilTrust/certificates/CertificateUpgradeable.sol
  • uri: it returns the base URI of the certificate.

    hardhat-env/contracts/OliveOilTrust/certificates/CertificateUpgradeable.sol

Testing the contracts

This application runs unit tests in all the roles, tokens, escrows, certificate and members contracts:

hardhat-env/test/index.ts

Since most of the contracts inherit from other base contracts, tests of contract functions are a call to a behaviour scenario function.

This testing strategy makes heavy use of fixtures for the local chain to reach the desired state before each test of a function.

For instance, let's focus on the test of the bottling plant contract. It exports a function called shouldBehaveLikeBottlingPlantUpgradeable that gathers two main describes: effects functions and view functions.

These obviously gather the functions that change the state of the chain and the view functions. So, for instance, we'll go over the test of the mint function:

hardhat-env/test/roles/BottlingPlantUpgradeable/BottlingPlantUpgradeable.behavior.ts

A fixture is loaded before running the test so that the bottling plant contract gets deployed, initialised and instructions for its types of tokens are set.

This means that an independent token, an industrial unit token and an industrial unit escrow will be deployed as well.

Furthermore, this fixture also waits for these contracts to mint tokens and transfer ownerships to the bottling plant contract.

This makes it all easy and fast for reaching a convenient state so that dependent tokens can be minted.

The function shouldBehaveLikeMint gets the parameters it needs to perform tests for three types of cases, or contexts:

  • suceeds: it gathers the tests that expect the function to succeed. For instance, this is the unit test for the mintage of a dependent token:

    hardhat-env/test/shared/base/DependentCreator/effects/mint.ts
  • fails: it gathers the tests that expect the function to fail. We can see below the test for the case when the ids and addresses arrays passed to the mint function are invalid:

    hardhat-env/test/shared/base/DependentCreator/effects/mint.ts
  • modifiers: this context gathers tests of all the modifiers that the function uses (mint only uses the modifier of access control onlyOwner):

    hardhat-env/test/shared/base/DependentCreator/effects/mint.ts

Deploying the contracts

The script hardhat-env/scripts/deploy.ts can be used to deploy the contracts of the members in Olive Oil Trust.

As we can see below, it will deploy all the contracts of the members and the contracts that these contracts need deployed in advance (tokens and escrows, or certificates) to pass their addresses.

hardhat-env/scripts/deploy.ts

As we deploy the contracts locally, localhost is passed as the network parameter in the following command:

The code above will write a JSON file for every contract deployed with the fields address, contractName, module, startBlock, abi, bytecode and deployedBytecode under a folder with the name of the network that is passed to the script.

This information will be used in the subgraph and front-end workspaces.

For instance, the subgraph will use data from the deployments to generate the subgraph manifest as we can see in the following post of this series of posts.


Related posts

Traceability

Introducing Olive Oil Trust

Introduction to a series of posts about Olive Oil Trust

Microservices

Approach to a microservices-based architecture bank application

A microservices-based architecture bank application that includes back-end and front-end applications, as well as a...

zk-SNARK

How to build a zero-knowledge DApp

This post offers an introduction to how to develop an application capable of generating and...

DeFi

End-to-end guide to creating an ERC-20 token-specific DApp

Decentralized application to operate with a mintable ERC-20 token


Ready to #buidl?

Are you seeking collaboration on a Web3 project or a developer to join your team?. Then, do not hesitate to contact me by e-mail or on my LinkedIn profile. You can also find me on GitHub.