WEB3DEV

Cover image for Contratos Inteligentes Atualizáveis: Aumentando a Flexibilidade e a Segurança
Isabela Curado Nehme
Isabela Curado Nehme

Posted on

Contratos Inteligentes Atualizáveis: Aumentando a Flexibilidade e a Segurança

A necessidade de capacidade de atualização e as abordagens para alcançá-la em contratos inteligentes.

07 de julho de 2023

https://miro.medium.com/v2/resize:fit:720/format:webp/1*Kofahqb0UI-44n8eZs8-LA.png

Os contratos inteligentes, os blocos de construção da blockchain, são programas de computador ou protocolos que funcionam em condições predeterminadas. Eles realizam transações e acompanham eventos com base nos termos do contrato. Uma vez implantados em uma blockchain, executam e aplicam automaticamente essas regras, sem a necessidade de intermediários. Isso elimina a necessidade de confiar em uma única parte.

Os contratos inteligentes e a tecnologia blockchain são descentralizados, o que significa que promovem transparência e imutabilidade. Depois que um contrato inteligente é implantado na blockchain, ele se torna uma entrada permanente no livro-razão e não pode ser alterado. Esta imutabilidade aumenta a segurança e a confiabilidade dos contratos inteligentes.

Necessidade de Capacidade de Atualização no Contrato Inteligente

A capacidade de atualização nos contratos inteligentes é crucial para corrigir bugs, abordar vulnerabilidades de segurança, adaptar-se às mudanças nos requisitos comerciais, melhorar a eficiência, incorporar padrões da indústria e preparar o contrato para o futuro. Permite que os desenvolvedores lancem versões atualizadas que melhoram a funcionalidade, a segurança e o desempenho ao longo do tempo. Além disso, a capacidade de atualização permite a governança comunitária, garantindo transparência e inclusão na definição da evolução dos contratos inteligentes. No geral, a capacidade de atualização fornece relevância, segurança e flexibilidade de contratos inteligentes no ecossistema dinâmico da blockchain.

Vamos nos aprofundar em uma exploração detalhada dos contratos inteligentes atualizáveis, incluindo sua definição, as abordagens usadas para implementá-los e as melhores práticas a serem seguidas ao projetar contratos inteligentes atualizáveis.

Abordagens Para Alcançar Capacidade de Atualização

Existem várias abordagens para implementar contratos inteligentes atualizáveis, cada uma com seus prós e contras. Aqui estão três abordagens comuns:

1. Padrão Proxy

https://miro.medium.com/v2/resize:fit:640/format:webp/0*jUpbrfOQxewlPAAZ.png

O padrão proxy envolve a separação do armazenamento e da lógica do contrato em dois acordos diferentes: um contrato proxy e um contrato de implementação. O contrato proxy atua como uma fachada e delega todas as chamadas ao contrato de implementação. Para atualizar o contrato, um novo contrato de implementação é implantado e o contrato proxy é atualizado para delegar chamadas à nova implementação.

Prós:

  • Separação de preocupações: a lógica e o armazenamento são separados, permitindo atualizações mais fáceis.
  • Interrupção mínima: as atualizações podem ser realizadas sem alterar o endereço do contrato, minimizando perturbações para os usuários.
  • Custos de gás reduzidos: apenas o contrato proxy precisa ser reimplantado durante as atualizações, economizando custos de gás.

Contras:

  • Complexidade: gerenciar o contrato proxy e sua interação com o contrato de implementação pode aumentar a complexidade do processo de desenvolvimento.
  • Acesso limitado ao armazenamento: os contratos atualizados podem ter acesso limitado ao armazenamento de versões anteriores, exigindo lógica de migração adicional.

Trecho:

// Contrato Proxy 
contract Proxy {
    address private implementation;

    function upgrade(address _newImplementation) public {
        implementation = _newImplementation;
    }

    fallback() external {
        address _impl = implementation;
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }
}

// Contrato de implementação 
contract MyContract {
    uint public value;

    function setValue(uint _newValue) public {
        value = _newValue;
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Padrão de Armazenamento Eterno

O padrão de armazenamento eterno separa o armazenamento do contrato de sua lógica, semelhante ao padrão proxy. No entanto, em vez de usar um contrato proxy, o armazenamento é armazenado em um contrato separado denominado “armazenamento eterno”. O contrato da lógica faz referência ao contrato de armazenamento, permitindo atualizações através da implantação de um novo contrato de lógica e redirecionando o contrato de armazenamento para o novo contrato de lógica.

Prós:

  • Separação de preocupações: a lógica e o armazenamento são separados, facilitando as atualizações.
  • Acesso aprimorado ao armazenamento: os contratos atualizados têm acesso total ao armazenamento de versões anteriores sem exigir lógica de migração.
  • Custos de implantação reduzidos: somente o contrato de lógica precisa ser reimplantado durante as atualizações, reduzindo os custos de implantação.

Contras:

  • Maior complexidade: a separação entre armazenamento e lógica pode aumentar a complexidade da base de código.
  • Confiança no contrato de armazenamento: o contrato de armazenamento eterno precisa ser cuidadosamente auditado e protegido, pois contém todos os dados do contrato.

Trecho:

// Contrato de armazenamento
contract EternalStorage {
    mapping(bytes32 => uint) private uintStorage;

    function getUint(bytes32 _key) public view returns (uint) {
        return uintStorage[_key];
    }

    function setUint(bytes32 _key, uint _value) public {
        uintStorage[_key] = _value;
    }
}

// Contrato de lógica
contract MyContract {
    EternalStorage private storageContract;

    constructor(address _storageContract) {
        storageContract = EternalStorage(_storageContract);
    }

    function getValue() public view returns (uint) {
        return storageContract.getUint(keccak256("value"));
    }

    function setValue(uint _newValue) public {
        storageContract.setUint(keccak256("value"), _newValue);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Bibliotecas Atualizáveis

Bibliotecas atualizáveis ​​envolvem a separação da lógica do contrato em bibliotecas separadas, que podem ser atualizadas de forma independente. O contrato principal faz referência às bibliotecas e delega chamadas para elas. As atualizações são realizadas através da implantação de novas versões da biblioteca e da atualização do contrato principal para usar as novas versões.

Prós:

  • Modularidade: as bibliotecas podem ser atualizadas de forma independente, permitindo atualizações mais flexíveis e granulares.
  • Reutilização de código: as bibliotecas podem ser compartilhadas em vários contratos, promovendo a reutilização de código.
  • Migração simplificada: as bibliotecas podem ter acesso ao armazenamento de versões anteriores sem exigir lógica de migração adicional.

Contras:

  • Complexidade de implantação: as atualizações exigem a implantação de novas versões de biblioteca e a atualização do contrato principal, o que pode ser mais complexo do que outras abordagens.
  • Aumento dos custos de gás: a delegação de chamadas para bibliotecas pode implicar custos adicionais de gás em comparação com a execução direta da lógica dentro do contrato.

Trecho:

// Contrato de biblioteca
library MyLibrary {
    function getValue(uint _input) public pure returns (uint) {
        // Lógica de implementação
        return _input * 2;
    }
}

// Contrato principal
contract MyContract {
    using MyLibrary for uint;
    uint public value;

    function setValue(uint _newValue) public {
        value = _newValue.getValue();
    }
} 
Enter fullscreen mode Exit fullscreen mode

Estas são três abordagens para implementar contratos inteligentes atualizáveis, cada uma com suas vantagens e desvantagens. A escolha depende dos requisitos e restrições específicas do seu projeto.

Melhores práticas ao projetar contratos inteligentes atualizáveis

Ao projetar contratos inteligentes atualizáveis, há várias considerações essenciais e práticas recomendadas a serem lembradas. A capacidade de atualização introduz complexidade e riscos potenciais; por isso, é crucial seguir estas diretrizes para garantir a segurança e o bom funcionamento do seu sistema de contrato inteligente. Aqui está um resumo das considerações essenciais e práticas recomendadas:

Declaração de Variável:

  1. Minimize variáveis ​​com estado: limite o uso de variáveis ​​com estado no contrato atualizável. Em vez disso, considere separar o estado do contrato da lógica do contrato e armazená-lo num contrato ou biblioteca separados. Essa separação ajuda a evitar perda de dados ou inconsistências durante as atualizações.
  2. Declare variáveis ​​como imutáveis: sempre que possível, declare as variáveis ​​como imutáveis ​​para garantir que não possam ser modificadas depois de inicializadas. Variáveis ​​imutáveis ​​melhoram a segurança do contrato e reduzem o risco de modificações não intencionais durante atualizações.
  3. Esteja atento à compatibilidade de variáveis: ao introduzir novas variáveis ​​de estado, certifique-se de que elas sejam compatíveis com as estruturas de variáveis existentes. Alterar os tipos ou layouts das variáveis em uma atualização pode causar problemas na recuperação ou manipulação de dados.
  4. Plano para Expansão Variável: projete contratos com expansão futura em mente. Considere possíveis requisitos futuros e permita a adição de novas variáveis ​​sem interromper a estrutura do contrato existente ou o armazenamento de dados.

Definindo Novas Funções:

  1. Use modificadores de função externa: considere marcar funções como external em vez de public ou private, quando possível. As funções externas são mais eficientes no uso de gás, pois não criam um contexto adicional para a chamada da função, o que pode ser importante ao implantar atualizações.
  2. Documentar interfaces de função: documente as interfaces de todas as funções, incluindo seus parâmetros, valores de retorno e comportamento esperado. Essa documentação ajuda a manter a consistência durante as atualizações e auxilia os desenvolvedores ou auditores a compreender o uso pretendido do contrato.
  3. Evite alterar assinaturas de funções existentes: quando uma função fizer parte da interface do contrato, evite alterar sua assinatura em atualizações subsequentes. Alterar a assinatura pode quebrar a compatibilidade com códigos ou interações existentes e pode exigir atualizações manuais ou migrações.
  4. Acompanhar versionamento semântico: use um esquema de versionamento semântico para indicar a compatibilidade de novas funções ou alterações introduzidas em atualizações. O versionamento semântico ajuda os usuários a compreender o impacto das atualizações e a gerenciar dependências de maneira eficaz.
  5. Testes abrangentes: teste minuciosamente as novas funções introduzidas nas atualizações, incluindo testes unitários e testes de integração. Os testes ajudam a garantir que as novas funções funcionem conforme pretendido e não introduzam vulnerabilidades ou comportamentos inesperados.
  6. Auditorias de segurança: envolva auditores de segurança independentes para revisar novas funções e sua integração no contrato inteligente atualizável. As auditorias fornecem uma perspectiva externa e ajudam a identificar possíveis falhas ou vulnerabilidades de segurança.
  7. Considere bibliotecas de funções externas: se possível, considere a utilização de bibliotecas externas para funções complexas ou usadas com frequência. Essa abordagem permite atualizar sua lógica de contrato inteligente, mantendo os contratos da biblioteca separados e atualizáveis ​​de forma independente.

Lembre-se de que as práticas recomendadas mencionadas aqui visam fornecer orientação geral, mas os requisitos específicos do seu projeto ou plataforma podem exigir desvantagens ("contras") adicionais.

Demonstração: Atualizando um Contrato Inteligente

Nesta seção, fornecerei um procedimento conciso e passo a passo para criar e implantar um contrato inteligente simples e atualizável. Além disso, irei orientá-lo sobre como atualizar o contrato de maneira eficaz.

Antes de começarmos, certifique-se de ter os seguintes pré-requisitos instalados:

  • Node.js (versão 12 ou superior)
  • Hardhat (versão 2 ou superior)

Vamos começar!

Etapa 1: Configurar o Projeto

  • Crie um novo diretório para o seu projeto e navegue até ele.
  • Inicialize um novo projeto Node.js executando o seguinte comando no terminal:
npm i -y

Enter fullscreen mode Exit fullscreen mode
  • Instale o Hardhat e os plugins necessários executando:
npm i --save-dev hardhat @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades dotenv

Enter fullscreen mode Exit fullscreen mode
  • Crie um projeto Hardhat executando o seguinte comando e selecionando suas opções preferidas:
npx hardhat
Enter fullscreen mode Exit fullscreen mode

Etapa 2: Escreva O Contrato Inicial

  • Crie um novo arquivo chamado 'MyContract.sol' no diretório de contratos (crie um se não existir).
  • Escreva seu contrato inicial em 'MyContract.sol'. Por exemplo, criarei um contrato ERC20 atualizável.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

contract MyContract is ERC20Upgradeable {
    uint256 public CONSTANT;
    uint256[50] __gap;

    function initialize(
        string memory name,
        string memory symbol
    ) public initializer {
        __ERC20_init(name, symbol);
        CONSTANT = 30;
    }
}
Enter fullscreen mode Exit fullscreen mode

No código acima, podemos observar duas coisas. Primeiro, declarei uma variável chamada '__gap'. Em contratos inteligentes atualizáveis, o array '__gap' é usado para reservar espaço de armazenamento para variáveis ​​futuras, garantindo um layout de armazenamento consistente em todas as versões do contrato. Evita colisões de armazenamento e comportamento inesperado ao adicionar ou modificar variáveis ​​durante as atualizações. Novas variáveis ​​podem ser adicionadas após o array __gap, utilizando o espaço reservado, sem afetar as variáveis ​​existentes.

Em segundo lugar, usei inicializar ao invés de um construtor. Em contratos inteligentes atualizáveis, a lógica do contrato pode ser atualizada preservando o armazenamento do contrato. Isso significa que, durante uma atualização, o construtor da nova lógica do contrato não seria chamado e a sua utilização poderia levar a consequências não intencionais ou inconsistências no estado do contrato.

Para resolver isso, uma função de inicialização é comumente usada em contratos atualizáveis. A função de inicialização atua como um substituto para o construtor e é chamada explicitamente após a implantação ou atualização do contrato. Permite a inicialização de variáveis ​​de estado ou qualquer outra configuração necessária específica para a versão atualizada.

Etapa 3: Implantar o Contrato Inicial

  • Crie um novo arquivo chamado my-contract.js na pasta de tarefas.
  • Adicione o seguinte código nesse arquivo para implantar o contrato inicial:
    task('deploy:my-contract', 'Deploy MyContract', async () => {
        const accounts = await ethers.getSigners();
        const signer = accounts[0];

        console.log('Implementando o contrato ...');

        const contractFactory = await ethers.getContractFactory('MyContract');
        const myContract = await upgrades.deployProxy(contractFactory, [
    'My Token',
    'MTK'
        ]);

        await myContract.deployed();

        console.info('Contrato implementado em ', myContract.address);
    });
Enter fullscreen mode Exit fullscreen mode
  • Configure seu arquivo .env.
    PRIVATE_KEY=

    ETHERSCAN_API_KEY=

    POLYSCAN_API_KEY=
    MUMBAI_ALCHEMY_API=
    SEPOLIA_ALCHEMY_API=
    MAINNET_ALCHEMY_API=

    DEPLOY_NETWORK=
Enter fullscreen mode Exit fullscreen mode
  • Configure seu hardhat.config.js. Você pode consultar o seguinte:
require('@nomicfoundation/hardhat-toolbox');
require('@openzeppelin/hardhat-upgrades');
require('dotenv').config();

require('./task/my-contract');

const PRIVATE_KEY = process.env.PRIVATE_KEY;
if (!PRIVATE_KEY) {
    console.error('Por favor, adicione PRIVATE_KEY ao .env');
    process.exit(1);
}
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY;
if (!ETHERSCAN_API_KEY) {
    console.error('Por favor, adicione ETHERSCAN_API_KEY ao .env');
    process.exit(1);
}

module.exports = {
    solidity: {
      version: '0.8.18',
      settings: {
        optimizer: {
          enabled: true,
          runs: 200,
        },
      },
    },
    networks: {
      hardhat: {
        chainId: 1337,
      },
      localhost: {
        chainId: 1337,
      },
      sepolia: {
        url: `https://eth-sepolia.g.alchemy.com/v2/${process.env.SEPOLIA_ALCHEMY_API}`,
        accounts: [PRIVATE_KEY],
        chainId: 11155111,
        gas: 'auto',
      },
      mainnet: {
        url: `https://eth-mainnet.g.alchemy.com/v2/${process.env.MAINNET_ALCHEMY_API}`,
        accounts: [PRIVATE_KEY],
        chainId: 1,
        gas: 'auto',
      },
    },
    etherscan: {
      apiKey: {
        sepolia: process.env.ETHERSCAN_API_KEY,
        mainnet: process.env.ETHERSCAN_API_KEY,
      },
    },
};

Enter fullscreen mode Exit fullscreen mode
  • Agora que tudo está configurado, vamos prosseguir e implantar nosso contrato executando o seguinte comando. (NOTA: estou implantando na rede Sepolia, você pode implantar na rede definida por você na configuração do Hardhat.)

    npx hardhat deploy:my-contract --network sepolia
    
    

Você deverá ver que seu contrato foi implantado e o endereço do contrato é exibido no terminal.

Após implantá-lo, também precisamos verificar nosso contrato.

  • Adicione a tarefa de verificação em my-contract.js presente na pasta de tarefas.
    task('verify:my-contract', 'Verify MyContract', async () => {
      await run('verify:verify', {
        address: '0x71AD25405e47c5605d74999f569ED1eCc0C2c4eF',
        constructorArguments: [],
      });
    });

Enter fullscreen mode Exit fullscreen mode
  • Execute a tarefa de verificação:
npx hardhat verify:my-contract --network sepolia
Enter fullscreen mode Exit fullscreen mode

Com isso, seu contrato foi implantado e verificado.

Etapa 4: Preparar o Contrato Atualizado

  • Atualize seu contrato no diretório de contratos.
  • Certifique-se de que tenha o mesmo nome e herança do contrato anterior.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

contract MyContract is ERC20Upgradeable {
    uint256 public CONSTANT;
    uint256 public MULTIPLIER;
    uint256[49] __gap;

    function initialize(
        string memory name,
        string memory symbol
    ) public initializer {
        __ERC20_init(name, symbol);
        CONSTANT = 30;
        MULTIPLIER = 2;
    }

    function multiply() public view returns (uint256) {
        return CONSTANT * MULTIPLIER;
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Adicione a tarefa de atualização em my-contract.js presente na pasta de tarefas.
task('upgrade:my-contract', 'Upgrade MyContract', async () => {
  const contractFactory = await ethers.getContractFactory('MyContract');
  const asset = await upgrades.upgradeProxy(
    'Seu endereço de contrato',
    contractFactory,
  );

  await asset.deployed();
  console.info('Seu contrato foi atualizado com sucesso');
});

Enter fullscreen mode Exit fullscreen mode
  • Execute a tarefa de atualização para implementar e vincular seu novo contrato de implementação ao proxy.
npx hardhat upgrade:my-contract --network sepolia

Enter fullscreen mode Exit fullscreen mode
  • Você também precisará verificar se o novo contrato de implementação está corretamente vinculado ao proxy.

É isso! Você atualizou um contrato inteligente com sucesso.

Concluindo

Assim, os contratos inteligentes atualizáveis ​​revolucionam a tecnologia blockchain, proporcionando flexibilidade e adaptabilidade. Eles permitem melhorias contínuas, correções de bugs e adição de novos recursos, mantendo a segurança e a imutabilidade. Isto abre possibilidades infinitas para desenvolvedores e usuários, moldando o futuro das aplicações descentralizadas.

Para obter mais atualizações sobre as ferramentas e tecnologias mais recentes, siga o blog Simform Engineering.

Siga-nos: Twitter | LinkedIn

Esse artigo foi escrito por Dhrumil Dalwadi e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Top comments (0)