Presentando Olive Oil Trust: contratos inteligentes

Ver en GitHub

Esta publicación es parte de una serie de publicaciones sobre Olive Oil Trust:

Los contratos inteligentes de Olive Oil Trust se implementan con el objeto de adoptar un conjunto de reglas para que todos los miembros de la cadena de suministro que tengan los mismos roles se establezcan rigurosamente dentro del mismo marco de trabajo.

En esta publicación explicaré cómo he diseñado, escrito, testeado e implementado estos contratos.

En este artículo

Diseñando los contratos

Para que una cuenta en Ethereum se convierta en miembro de Olive Oil Trust con una función específica, debe implementar un contrato que herede del contrato de ese rol (los contratos están escritos en el lenguaje Solidity).

Los miembros tienen la propiedad de sus propios tokens y escrows (o certificados en el caso de los certificadores) para realizar sus acciones y establecer estados consecuentemente.

Digamos que somos una planta embotelladora llamada Bottling Company y nos gustaría adoptar este rol en Olive Oil Trust. Tendríamos que desplegar el siguiente contrato:

hardhat-env/contracts/members/BottlingCompany.sol

Como podemos ver esto requeriría primero el despliegue de dos contratos de unos tokens y un contrato de un escrow para pasar sus direcciones como argumentos e inicializar el contrato.

Además, los contratos de Olive Oil Trust están diseñados para su uso en contratos actualizables, como podemos ver en el bloque de código de arriba ya que el contrato hereda del Initializable.sol de OpenZeppelin y hay un función initialize en lugar de un constructor.

Olive Oil Trust utiliza contratos actualizables para dar la posibilidad a sus miembros de actualizar sus contratos a una versión más nueva.

Al desplegar un contrato actualizable con los plugins de actualización de OpenZeppelin, se implementa un proxy que no puede ser modificado.

En cambio, el contrato de implementación al que se envían las transacciones se reemplazaría por uno nuevo si tuviéramos que actualizar los contratos.

Por favor, visite esta entrada en el blog de OpenZeppelin para obtener más información.

La mayoría de los contratos siguen un patrón de proxy UUPS; sin embargo, debido a los límites de tamaño, algunos otros siguen un patrón de proxy transparente.

Los contratos que conforman Olive Oil Trust se muestran a continuación:

Roles

Hay siete roles en Olive Oil Trust: olivareros, almazaras, fabricantes de botellas, plantas embotelladoras, distribuidores, minoristas y certificadores.

Puedes encontrarlos todos en hardhat-env/contracts/OliveOilTrust/roles.

Heredan de los contratos base, así como de los contratos actualizables en utils y access de OpenZeppelin.

Por ejemplo, centrémonos en el contrato para el rol de la planta embotelladora y repasemos sus principales aspectos y funciones no privadas.

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

Como podemos ver este contrato hereda de seis contratos:

  • @openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol: este es un contrato base que nos ayuda a escribir contratos actualizables mediante el uso de modificadores.

  • @openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol: un mecanismo de actualización diseñado para el proxy UUPS. Para usarlo correctamente, la función _authorizeUpgrade debe sobreescribirse para incluir la restricción de acceso al mecanismo de actualización, como podemos ver en la línea 53.

  • hardhat-env/contracts/OliveOilTrust/base/DependentCreator.sol: este contrato reúne funciones con respecto a la acuñación de tokens dependientes, que veremos más adelante en esta publicación.

  • hardhat-env/contracts/OliveOilTrust/base/BaseSeller.sol: contrato base que implementa acciones realizadas por un vendedor en la cadena de valor. Interactúa con los contratos de tokens y de escrow propiedad del miembro en cuestión.

  • @openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol: permite que el contrato tenga tokens ERC1155.

  • @openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol: proporciona un mecanismo de control de acceso.

__BottlingPlantUpgradeable_init reúne llamadas linealizadas a los inicializadores principales, mientras que __BottlingPlantUpgradeable_init_unchained equivale a la función inicializadora menos las llamadas a los inicializadores principales y reúne la lógica que se hallaría en un constructor.

Las funciones de inicialización no son linealizadas por el compilador, como sí lo son los constructores. Este mecanismo, que he replicado de los contratos actualizables de OpenZeppelin, se usa para evitar inicializar el mismo contrato dos veces.

El contrato almacena las direcciones de industrialUnitToken_ y escrow_ en variables de estado y otorga permisos para:

  • que escrow_ opere con los tokens de la planta embotelladora en industrialUnitToken_. El contrato de escrow debe poder transferirse a sí mismo los tokens de la planta embotelladora en el momento de depositarlos.

  • que industrialUnitToken_ opere con los tokens de la planta embotelladora en dependentToken. El contrato del token de unidad industrial debe poder transferirse los tokens de la planta embotelladora al empacarlas.

Veamos ahora las funciones heredadas de DependentCreator.sol (ya que el resto de las funciones simplemente llaman a las funciones respectivas de tokens y escrows y se basan en su implementación, que veremos más adelante).

Estas funciones, que dependen enteramente de su implementación en DependentCreator.sol, son:

  • setTokenTypeInstructions y setTokenTypesInstructions: estas funciones establecen un conjunto, o múltiples conjuntos, de instrucciones para que un miembro DependentCreator puede validar la acuñación de un nuevo token de ese tipo. Ambas funciones dependen completamente de su implementación en hardhat-env/contracts/OliveOilTrust/tokens/DependentTokenUpgradeable.sol que veremos más adelante en tokens.
hardhat-env/contracts/OliveOilTrust/roles/DependentCreator.sol
  • mint y mintBatch: acuñan uno o varios lotes de un tipo de token.

Aunque, podría confundir que mint acuña un lote de un token y mintBatch acuña varios lotes de un tipo de token, esto es para continuar con la nomenclatura utilizada por OpenZeppelin en la implementación del estándar de múltiples tokens ERC-1155.

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

Por cada lote que se va a acuñar, estas funciones validan que tenemos suficientes unidades de los tokens requeridos para acuñar los tokens que necesitamos.

Validation.validate cumple los siguientes propósitos:

  1. Realiza comprobaciones de seguridad para garantizar que todas las dimensiones de los arrays pasados sean válidas y revierte en caso contrario.

  2. Obtiene las instrucciones del tipo de token que se va a validar y garantiza que los tokens que se utilizan para acuñar el token dependiente cumplan con estas instrucciones.

  3. Si los pasos antes mencionados tienen éxito, consume la cantidad indicada de cada token de entrada utilizado en el proceso.

Podemos ver la implementación a continuación:

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

Si la dirección del token a consumir en cualquier dimensión e índice no coincide con la dirección establecida en las instrucciones, se asume que estas instrucciones utilizan un certificado/s para ese tipo de token.

Entonces, la función validate consulta dicho/s certificado/s si la/s dirección/es del token, o tokens, a consumir está/están certificadas.

Una vez validados los parámetros del token, o tokens, a acuñar, un evento o múltiples eventos de tipo TokenAncestrySet es/son emitidos. Luego, se acuña el/los lote/s de token/s.

Este evento almacena información sobre la ancestría del token para poder realizar fácilmente un seguimiento del token hasta su origen.

Tokens

Hay tres tipos de tokens:

  1. Tokens independientes: tokens que no requieren la acuñación de otros tokens.

  2. Tokens dependientes: tokens que son el resultado de la transformación de otros tokens, siendo de esta marera dependendientes de su disponibilidad.

  3. Tokens de unidades industriales: tokens que envuelven tokens que representan unidades comerciales (tokens de botellas de aceite de oliva).

Con el fin de simplificar el proceso y de alguna manera establecer el origen desde donde comenzar a rastrear un producto, los oleicultores y los fabricantes de botellas acuñan tokens independientes en Olive Oil Trust, es decir tokens originarios.

La implementación de un token independiente es más simple que la de un token dependiente, ya que no hay que establecer instrucciones ni validar la acuñación de tokens, así que veamos el código de tokens dependientes y de unidades industriales.

Tokens dependientes

Las principales funciones en DependentTokenUpgradeable.sol son:

  • setTokenTypeInstructions ysetTokenTypesInstructions: comprueban que los parámetros pasados son válidos, que los id(s) del tipo/s de token/s no están duplicados y, además, establecen las instrucciones en una variable de estado mapping y emite un evento o múltiples eventos de tipo TokenTypeInstructionsSet para que las instrucciones puedan ser indexadas en los mappings de los subgrafos.

    hardhat-env/contracts/OliveOilTrust/tokens/DependentTokenUpgradeable.sol
  • mint y mintBatch: acuñan uno o varios lotes de tokens. Los tokens de un lote pueden ser no fungibles o fungibles dependiendo de si solo se acuñan una o varias unidades, respectivamente.

    Todas las unidades de un mismo lote son similares y, por tanto, fungibles. Sin embargo, son diferentes de las de otros lotes incluso si comparten el mismo tipo de token.

    hardhat-env/contracts/OliveOilTrust/tokens/DependentTokenUpgradeable.sol
  • getInstructions: esta es una función de visualización que devuelve las instrucciones de un lote determinado.

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

Tokens de unidades industriales

Una diferencia notable entre los tokens de unidades industriales e independientes es que, aunque ambos implementan tokens ERC-1155, IndustrialUnitTokenUpgradeable.sol está habilitado para poseer tokens ERC-1155 (hereda de OpenZeppelin ERC1155HolderUpgradeable.sol).

En otras palabras, la propiedad de un token ERC-1155 se puede transferir a la dirección de IndustrialUnitTokenUpgradeable.sol.

Esto es necesario para cumplir con la lógica de empaque y desempaque que se tiene que implementar.

Las principales funciones del contrato son:

  • pack y packBatch transfieren la propiedad de los tokens a envasar desde la planta embotelladora a la dirección de la unidad industrial propiedad de la planta embotelladora. Emite un evento de tipo SinglePacked o BatchPacked.

    hardhat-env/contracts/OliveOilTrust/tokens/IndustrialUnitTokenUpgradeable.sol
  • unpack y unpackBatch transfieren la propiedad del token/s a envasar desde la dirección de la unidad industrial propiedad de la planta embotelladora a la planta embotelladora. Luego se queman los tokens de las unidades industrial(es). Emite un evento de tipo SingleUnpacked o BatchUnpacked.

    hardhat-env/contracts/OliveOilTrust/tokens/IndustrialUnitTokenUpgradeable.sol
  • El resto son tres funciones de visualización para obtener información sobre los tokens.

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

Escrows

Los escrows son los contratos que los miembros de Olive Oil Trust usan para intercambiar tokens. Por lo tanto, son un mecanismo para transferir la propiedad de un token, o tokens, a cambio de una cantidad de ether pero sin la participación de terceros.

Hay tres escrows diferentes:

  • AgriculturalEscrowUpgradeable.sol: este escrow está destinado a la posesión de tokens de aceitunas en la fase agrícola de la cadena de valor del aceite de oliva. Hay un escrow diferente para mantener este token independiente porque el precio lo establece la almazara después de verificar las cualidades de las aceitunas que está considerando comprar. Esto es similar a hacer una oferta al olivarero por la propiedad de su producto.

  • CommercialUnitsEscrowUpgradeable.sol: este tipo de escrow está diseñado para albergar el resto de tipos de tokens excepto las unidades industriales. Es similar al escrow descrito anteriormente, excepto que el precio lo establece el vendedor.

  • IndustrialUnitsEscrowUpgradeable.sol: este escrow está destinado a la posesión de tokens de unidades industriales. La principal diferencia con los otros escrows es que no requiere que se especifique una cantidad para cada token que se deposite, ya que todas las unidades industriales se acuñan como una sola unidad.

Por ejemplo, una planta embotelladora posee un contrato de escrow que hereda de IndustrialUnitsEscrowUpgradeable.sol ya que vende unidades industriales a distribuidores.

Este tipo de escrow también hereda del contrato de OpenZeppelin ERC1155HolderUpgradeable.sol porque el escrow se convertirá en propietario de los tokens que se depositen hasta que se cierre el escrow:

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

El proceso de compra de un token se representa en hasta tres de seis etapas diferentes:

  1. Se deposita uno o varios tokens, lo que establece el estado del escrow en Active.

    • El depósito del token, o tokens, ahora podría revertirse antes de que un miembro depositase ether, lo que pondría el estado en un estado final RevertedBeforePayment y transferiría el token, o los tokens, al vendedor.
  2. Se deposita la cantidad requerida de ether para comprar el/los token/s, lo que establece el estado en EtherDeposited.

    • El depósito de ether ahora podría ser cancelado por el comprador candidato, lo que haría que el estado volviera a Active y transferiría los fondos de vuelta al comprador.

    • El depósito de los tokens también podría revertirse después de que se depositase ether, lo que establecería el estado en un estado final RevertedAfterPayment, transferiría los fondos al comprador y los tokens al vendedor.

  3. La operación de escrow se completa y el comprador y el vendedor obtienen token/s y ether respectivamente. El estado del escrow finalmente se establece en Closed.

RevertedBeforePayment, RevertedAfterPayment y Closed son estados finales, lo que significa que una vez que se alcanzan estos estados, el escrow no se puede modificar más, y ya no poseerá ni ether ni tokens.

Cada etapa se alcanza interactuando con el escrow a través de un conjunto de funciones.

Como veremos a continuación, existe una comprobación al inicio de cada función (antes de cualquier transferencia) que hace que se revierta si el estado para realizar la acción llamada no es el requerido.

Por ejemplo, un escrow no se puede cerrar si no se han depositado los fondos.

Cada función individual emite un evento de su tipo para almacenar la información requerida del escrow para que pueda ser indexada por los mappings de los subgrafos.

Repasemos estas funciones:

  • depositToken and depositBatch: estas funciones depositan uno o más tokens en el escrow. Las funciones realizan comprobaciones de seguridad para evitar que información no válida se establezca en el estado del escrow. Luego, el estado del escrow cambia a Active. Por último, los tokens se transfieren al contrato de escrow. Esto es posible porque se otorgaron permisos a tal efecto, como vimos anteriormente, en BottlingPlantUpgradeable-__BottlingPlantUpgradeable_init_unchained. Solo pueden ser llamadas por el propietario del contrato, es decir, el vendedor. Su implementación se muestra a continuación:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • revertBeforePayment: establece el estado del escrow a RevertedBeforePayment y transfiere los tokens al vendedor. No es necesario realizar más acciones ya que no se han depositado fondos en este punto en el tiempo. Solo puede ser llamado por el propietario del contrato, es decir, el vendedor. Compruebe la implementación a continuación:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • makePayment: establece el estado en EtherDeposited y transfiere ether al escrow. Solo acepta la misma cantidad de ether que el precio establecido por el vendedor. Podemos ver la implementación a continuación:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • cancelPayment: cancela el pago, es decir, vuelve a poner el estado del escrow en Active y devuelve los fondos al comprador candidato. Solo la misma dirección que depositó los fondos puede cancelar ese pago. Compruebe la implementación a continuación:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • revertAfterPayment: establece el estado en RevertedAfterPayment, transfiere los fondos al comprador candidato y los token/s al vendedor. Solo puede ser llamado por el propietario del contrato, es decir, el vendedor. Podemos ver la implementación a continuación:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • close: establece el estado en Closed, transfiere los fondos al vendedor y el/los token/s al comprador. Solo puede ser llamado por el propietario del contrato, es decir, el vendedor. Compruebe la implementación a continuación:

    hardhat-env/contracts/OliveOilTrust/escrows/IndustrialUnitsEscrowUpgradeable.sol
  • escrow y state: estas funciones de visualización devuelven los campos del struct MyIndustrialUnitsEscrow de un escrow determinado y el estado de este escrow, respectivamente. Compruebe la implementación a continuación:

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

Certificado

El contrato de certificado permite al certificador atestigüar la validez de un tipo de token a un cierto estándar.

Como podemos ver a continuación, hereda de UUPSUpgradeable (por lo que sobreescribe _authorizeUpgrade) y necesita un URI base para inicializar el contrato:

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

CertificateUpgradeable.sol proporciona al titular del contrato, un certificador, la función certifyToken para validar un tipo de token y certifyBatch para validar más de un tipo de token.

Ambos emiten eventos para almacenar información sobre la/s certificación/es:

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

CertificateUpgradeable.sol también implementa tres funciones de visualización:

  • isCertified: devuelve si un tipo de _token _está certificado.

    hardhat-env/contracts/OliveOilTrust/certificates/CertificateUpgradeable.sol
  • certificatesOf y certificatesOfBatch: la primera devuelve los certificados de un tipo de token y la segunda múltiples tipos de tokens.

    hardhat-env/contracts/OliveOilTrust/certificates/CertificateUpgradeable.sol
  • uri: devuelve la URI base del certificado.

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

Testeando los contratos

Esta aplicación ejecuta pruebas unitarias en todos los roles, tokens, escrows, certificados y contratos de miembros:

hardhat-env/test/index.ts

Dado que la mayoría de los contratos se heredan de otros contratos base, los tests de las funciones de los contratos son una llamada a una función de un escenario de comportamiento.

Esta estrategia de test hace un uso intensivo de fixtures para que la cadena local alcance el estado deseado antes de cada test de una función.

Por ejemplo, centrémonos en el test del contrato de la planta embotelladora. Exporta una función llamada shouldBehaveLikeBottlingPlantUpgradeable que reúne dos describes: funciones de efectos y funciones de visualización.

Estos obviamente reúnen las funciones que cambian el estado de la cadena y las funciones de visualización. Entonces, por ejemplo, repasaremos el test de la función mint:

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

Se carga un fixture antes de ejecutar el test para que el contrato de la planta embotelladora se implemente, se inicialice y se establezcan las instrucciones para sus tipos de tokens.

Esto significa que también se desplegarán un token independiente, un token de unidad industrial y un escrow de unidad industrial.

Además, este fixture también espera a que estos contratos acuñen tokens y transfieran la propiedad al contrato de la planta embotelladora.

Esto hace que sea fácil y rápido alcanzar un estado conveniente para que se puedan acuñar tokens dependientes.

La función shouldBehaveLikeMint obtiene los parámetros que necesita para realizar test para tres tipos de casos o contextos:

  • suceeds: reúne los test que esperan que la función tenga éxito. Por ejemplo, esta es el test unitario para la acuñación de un token dependiente:

    hardhat-env/test/shared/base/DependentCreator/effects/mint.ts
  • fails: recoge los tests que esperan que la función falle. Podemos ver a continuación el test para el caso en que los arrays de ids y direcciones pasados a la función mint no son válidos:

    hardhat-env/test/shared/base/DependentCreator/effects/mint.ts
  • modificadores: este contexto reúne tests de todos los modificadores que utiliza la función (mint solo utiliza el modificador de control de acceso onlyOwner):

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

Desplegando los contratos

El script hardhat-env/scripts/deploy.ts se puede utilizar para desplegar los contratos de los miembros de Olive Oil Trust.

Como podemos ver a continuación, desplegará todos los contratos de los miembros y los contratos que estos contratos necesitan desplegar de antemano (tokens y escrows, o certificados) para pasar sus direcciones.

hardhat-env/scripts/deploy.ts

Dado que desplegamos los contratos localmente, localhost se pasa como parámetro de red en la siguiente instrucción:

El código anterior escribirá un archivo JSON para cada contrato implementado con los campos address, contractName, module, startBlock, abi, bytecode y deployedBytecode en una carpeta con el nombre de la red que se pasa al script.

Esta información se utilizará en el subgrafo y en los workspaces del front-end.

Por ejemplo, el subgrafo utilizará datos de los despliegues para generar el manifiesto del subgrafo, como podemos ver en el siguiente artículo de esta serie de publicaciones.


Artículos relacionados

DeFi

Guía de principio a fin para crear una DApp específica para un token ERC-20

Aplicación descentralizada para operar con un token ERC-20 acuñable

Trazabilidad

Presentando Olive Oil Trust

Introducción a una serie de artículos acerca de Olive Oil Trust

zk-SNARK

Cómo desarrollar una DApp de conocimiento cero

Esta entrada ofrece una introducción a cómo desarrollar una aplicación capaz de generar y validar...

Microservicios

Propuesta de una aplicación bancaria con arquitectura basada en microservicios

Aplicación bancaria de arquitectura basada en microservicios que incluye aplicaciones de back end, front end,...


¿Preparado para #buidl?

¿Está interesado en Web3 o en las sinergias entre la tecnología blockchain, la inteligencia artificial y el conocimiento cero?. Entonces, no dude en contactarme por e-mail o en mi perfil de LinkedIn. También me puede encontrar en GitHub.