WEB3DEV

Cover image for Padrões de Projeto para Contratos Inteligentes - Manutenção
Fatima Lima
Fatima Lima

Posted on

Padrões de Projeto para Contratos Inteligentes - Manutenção

Image description

Introdução

Esta é a quarta seção da série de cinco partes sobre como resolver falhas de projeto recorrentes por meio de padrões de projeto reutilizáveis e convencionais. Para isso, dissecaremos em Manutenção um grupo de padrões que fornecem mecanismos para contratos operacionais ativos. Em contraste com os aplicativos distribuídos comuns, que podem ser atualizados quando são detectados erros, os contratos inteligentes são irreversíveis e imutáveis. Isso significa que não há como atualizar um contrato inteligente, a não ser escrevendo uma versão aprimorada que é então implantada como um novo contrato.

Segregação de Dados

Problema

Os dados do contrato e sua lógica geralmente são mantidos no mesmo contrato, o que leva a um acoplamento bastante emaranhado.

Ao atualizar um contrato inteligente, o que realmente acontece é que uma nova versão do contrato é implantada na rede e ela coexiste com a versão antiga. Como o contrato antigo não é de fato atualizado para uma nova versão, o armazenamento acumulado ainda reside no endereço antigo. Isso geralmente inclui dados importantes, como informações do usuário, saldos de contas ou referências a outros contratos, que ainda são necessários na nova versão do contrato.

A gravação no armazenamento é uma das operações mais caras na Ethereum. Ler sucessivamente cada entrada de armazenamento e armazená-la no novo endereço, sempre que um contrato for atualizado, não seria razoável do ponto de vista econômico.

Além disso, haveria até mesmo uma chance de a transação que realiza a migração de armazenamento ficar sem gas, caso haja muitas entradas para armazenar.

Além disso, toda a migração do armazenamento precisaria ser planejada durante o tempo de criação e muita lógica adicional precisaria ser incluída para realizar a migração.

Solução

O padrão de segregação de dados separa a lógica do contrato de seus dados subjacentes. A segregação promove a separação das preocupações e imita um projeto em camadas (por exemplo, camada de lógica, camada de dados).

É conveniente projetar o contrato de armazenamento de forma muito genérica para que, depois de criado, ele possa armazenar e acessar diferentes tipos de dados com a ajuda de métodos setter e getter.

Camada de Armazenamento:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;

contract DataStorage {
   mapping(bytes32 => uint256) uintStorage;

   function getUintValue(bytes32 key) public returns (uint256) {
       return uintStorage[key];
   }

   function setUintValue(bytes32 key, uint256 value) public {
       uintStorage[key] = value;
   }
}
Enter fullscreen mode Exit fullscreen mode

Camada de Lógica:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
import "./DataStorage.sol";

contract Logic {
   DataStorage dataStorage;

   constructor(address _address) {
       dataStorage = DataStorage(_address);
   }

   function f() public {
       bytes32 key = keccak256(abi.encode("ACCOUNT_BALANCES", msg.sender));
       dataStorage.setUintValue(key, 100 ether);
       dataStorage.getUintValue(key);
   }
}
Enter fullscreen mode Exit fullscreen mode

Satellite

Problema

Os contratos são imutáveis. A funcionalidade de alteração do contrato exige a implantação de um novo contrato.

Solução

O padrão satellite (satélite) permite modificar e substituir a funcionalidade do contrato. Isso é feito por meio da criação de contratos satélite separados que encapsulam determinadas funcionalidades do contrato. Os endereços desses contratos satélite são armazenados em um contrato básico.

Esse contrato pode, então, chamar os contratos satélite quando precisar fazer referência a determinadas funcionalidades, usando os ponteiros de endereço armazenados. Porém, quando essas funções precisarem alterar sua lógica devido a melhorias na segurança ou ajustes nos requisitos comerciais etc., modificar a funcionalidade é tão simples quanto criar novos contratos satélite e alterar os endereços satélite correspondentes.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
import "@openzeppelin/contracts/access/Ownable.sol";

contract Base is Ownable {
   uint256 public variable;
   address satelliteAddress;

   function setVariable() public onlyOwner {
       Satellite s = Satellite(satelliteAddress);
       variable = s.calculateVariable();
   }
   function updateSatelliteAddress(address _address) public onlyOwner {
       satelliteAddress = _address;
   }
}

contract Satellite {
   function calculateVariable() public pure returns (uint256){
       // calcula var
       return 2 * 3;
   }
}
Enter fullscreen mode Exit fullscreen mode

Registro do Contrato

Problema

Os participantes do contrato devem ser encaminhados para a versão mais recente do contrato.

Solução

O padrão de registro é uma abordagem para lidar com o processo de atualização de um contrato. O padrão mantém o controle de diferentes versões (endereços) de um contrato e aponta, quando solicitado, para a versão mais recente. Antes de interagir com um contrato, o usuário sempre terá de consultar o registro para obter o endereço mais recente do contrato. Também é importante determinar como lidar com os dados existentes do contrato, quando uma versão antiga do contrato é substituída.

Uma solução alternativa para apontar para o endereço do contrato mais recente seria utilizar o Ethereum Name Service (ENS).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./ImplementationContract.sol";

contract Factory is Ownable {
   address implementation;
   address[] previousImpl;

   event UpdateImplementation(address indexed oldImpl, address indexed newImpl);

   function getLatestVersion() external view returns(address) {
       return implementation;
   }

   function createNewVersion(
       /** parâmetros */
   ) external onlyOwner {
       /** Pré-checagem da validade dos parâmetros */

       bytes memory bytecode = type(ImplementationContract).creationCode;
       /** Concatena creationCode com parâmetros do construtor de 32-bytes */
       bytecode = abi.encodePacked(
           bytecode,
           abi.encode(/** parâmetros do construtor */)
       );

       bytes32 salt = keccak256(
           abi.encodePacked(/** materiais aleatórios. e.g.[block.timestamp, _msgSender(), etc] */)
       );
       address newImplAddress;

       assembly {
           newImplAddress := create2(0, add(bytecode, 32), mload(bytecode), salt)
       }

       /** atualiza a variável de estado */
       previousImpl.push(implementation);
       implementation = newImplAddress;

       ImplementationContract(implementation).initialize(
          /** parâmetros inicializados */
       );

       /** Criando uma lógica adicional */

       emit UpdateImplementation(previousImpl[previousImpl.length - 1], implementation);
   }
}
Enter fullscreen mode Exit fullscreen mode

Retransmissão de Contrato

Problema

Os participantes do contrato devem ser encaminhados para a versão mais recente do contrato.

Solução

Um retransmissor é outra abordagem para lidar com o processo de atualização de um contrato. O padrão de retransmissão fornece um método para atualizar um contrato (contrato de implementação) para uma versão mais recente e, ao mesmo tempo, manter o endereço do contrato antigo (endereço Proxy). Isso é obtido com o uso de um tipo de contrato proxy que encaminha chamadas e dados para a versão mais recente do contrato.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

abstract contract Proxy {
   /**
    * @dev Informa o endereço da implementação em que cada chamada será delegada.
    * @return O endereço da implementação ao qual será delegada
    */
   function implementation() public view virtual returns (address);

   function _fallback() internal {
       address _impl = implementation();
       require(_impl != address(0), "Implementation is invalid");

       assembly {
           let ptr := mload(0x40)
           calldatacopy(ptr, 0, calldatasize())
           let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0)
           let size := returndatasize()
           returndatacopy(ptr, 0, size)

           switch result
           case 0 { revert(ptr, size) }
           default { return(ptr, size) }
       }
   }

   /**
    * @dev Recebe a função permitindo realizar uma delegatecall para a implementação fornecida.
    * Essa função retornará o que a chamada de implementação retornar
    */
   receive() external payable {
       _fallback();
   }

   /**
    * @dev Recebe a função permitindo realizar uma delegatecall para a implementação fornecida.
    * Essa função retornará o que a chamada de implementação retornar
    */
   fallback() external payable {
       _fallback();
   }
}
Enter fullscreen mode Exit fullscreen mode

Outra desvantagem dessa abordagem é que o layout de armazenamento de dados precisa ser consistente nas versões mais recentes do contrato, caso contrário, os dados podem ser corrompidos, o que significa que a sequência de armazenamento não deve ser alterada, apenas adições são permitidas.

Conclusão

Descrevi o grupo de padrões de Manutenção em detalhes e forneci um código exemplar para melhor ilustração. Recomendo que você use pelo menos um desses padrões em seu próximo projeto Solidity para testar sua compreensão desse tópico. Na próxima postagem, passaremos para o próximo e último grupo de padrões, o Security.

Siga-me no Linkedin para Ficar Conectado

https://www.linkedin.com/in/ninh-kim-927571149/

Esse artigo foi escrito por BrianKim e traduzido por Fátima Lima. O original pode ser lido aqui.

Latest comments (0)