WEB3DEV

Cover image for Tornando Seu Contrato Inteligente Solidity Atualizável
Panegali
Panegali

Posted on

Tornando Seu Contrato Inteligente Solidity Atualizável

E o prompt do ChatGPT usado


No mundo das blockchains, os contratos inteligentes são a base para qualquer aplicativo descentralizado. Assim como qualquer software, os contratos inteligentes também precisam de atualizações regulares e correções de erros. No entanto, devido à natureza imutável da blockchain, alcançar isso não é simples. Este guia irá orientá-lo sobre como tornar seus contratos inteligentes atualizáveis, garantindo um ciclo de desenvolvimento mais suave e preparado para o futuro.

Este artigo faz parte de uma série de artigos em que tentamos desenvolver um "banco cripto". Você pode conferir livremente o primeiro artigo que apresenta a ideia aqui. Nesta parte, apresentaremos inicialmente o contrato inteligente original que foi usado. Em seguida, mostraremos como o tornamos atualizável usando um prompt específico fornecido ao ChatGPT. E, por fim, um exemplo de adição de funcionalidade no contrato inteligente.

O Contrato Inteligente Original

Nosso ponto de partida é um contrato inteligente que funciona como um sistema de cheque, onde os usuários podem depositar e sacar fundos e criar "cheques" para que outros possam reivindicar. Aqui está o contrato original, escrito em Solidity:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract CheckClaim {
    using ECDSA for bytes32;

    mapping(address => uint256) public balances;
    mapping(address => mapping(uint256 => bool)) public usedNonces;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Saldo insuficiente");
        balances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
    }

    function depositCheck(address recipient, bytes memory signature, uint256 amount, uint256 nonce) public {
        bytes32 message = prefixed(keccak256(abi.encodePacked(recipient, nonce, amount)));
        address signer = recoverSigner(message, signature);

        // Defina o destinatário como o endereço do remetente se ele estiver definido como address(0)
        // Isso permite que qualquer pessoa reivindique o cheque!
        if (recipient == address(0)) {
            recipient = msg.sender;
        }

        require(!usedNonces[signer][nonce], "O cheque ja foi usado");
        require(signer != address(0), "Assinatura invalida");
        require(balances[signer] >= amount, "Saldo insuficiente");

        balances[signer] -= amount;
        balances[recipient] += amount;
        usedNonces[signer][nonce] = true;
    }

    function claimCheckAndWithdraw(address recipient, bytes memory signature, uint256 amount, uint256 nonce) public {
        // Primeiro, solicite o cheque usando a função depositCheck
        depositCheck(recipient, signature, amount, nonce);

        // Em seguida, retire o valor reivindicado para o saldo real do usuário usando a função withdraw existente
        withdraw(amount);
    }

    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return hash.toEthSignedMessageHash();
    }

    function recoverSigner(bytes32 message, bytes memory signature) internal pure returns (address) {
        return message.recover(signature);
    }
}
Enter fullscreen mode Exit fullscreen mode

Em detalhes, as principais funções do contrato são:

  • deposit(): esta função permite que um usuário deposite ethers no contrato. Os ethers são adicionados ao saldo do usuário no contrato.
  • withdraw(uint256 amount): esta função permite que um usuário retire ethers do seu saldo no contrato.
  • depositCheck(address recipient, bytes memory signature, uint256 amount, uint256 nonce): esta função permite que um usuário deposite um cheque que pode ser reivindicado por um destinatário. Ela verifica a assinatura do cheque e adiciona o valor do cheque ao saldo do destinatário no contrato.
  • claimCheckAndWithdraw(address recipient, bytes memory signature, uint256 amount, uint256 nonce): esta função é uma combinação de depositCheck e withdraw. Ela permite que um destinatário reivindique um cheque e retire o valor do cheque do seu saldo no contrato.

O contrato também inclui duas funções auxiliares: recoverSigner(bytes32 message, bytes memory signature) e prefixed(bytes32 hash). Essas funções são usadas para verificar a assinatura do cheque na função depositCheck.

Tornando o Contrato Atualizável

Para tornar este contrato atualizável, usaremos o padrão de Proxy juntamente com as bibliotecas TransparentUpgradeableProxy, Initializable e TimelockController do OpenZeppelin. O novo contrato será controlado por um único administrador que pode iniciar atualizações que entram em vigor após um intervalo mínimo de 72 horas.

Aqui está o contrato modificado em três partes. Primeiro, o contrato atualizado:

// O contrato original atualizado
// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract CheckClaimV1 is Initializable {
    using ECDSA for bytes32;

    mapping(address => uint256) public balances;
    mapping(address => mapping(uint256 => bool)) public usedNonces;

    function initialize() public initializer {
    }

    // Outras funções do contrato original devem ser colocadas aqui
    // deposit, withdraw, ...
}
Enter fullscreen mode Exit fullscreen mode

O proxy:

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

import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

contract YourProxy is TransparentUpgradeableProxy {
    constructor(address _logic, address _admin, bytes memory _data) 
        TransparentUpgradeableProxy(_logic, _admin, _data) {
    }
}
Enter fullscreen mode Exit fullscreen mode

O TimeLockController:

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

import "@openzeppelin/contracts/governance/TimelockController.sol";

contract YourTimelockController is TimelockController {
    constructor(uint256 minDelay, address[] memory proposers, address[] memory executors) 
        TimelockController(minDelay, proposers, executors) {
        renounceRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }
}
Enter fullscreen mode Exit fullscreen mode

Isso foi criado com a execução do seguinte prompt para o ChatGPT:

Projete um contrato inteligente Solidity atualizável que seja compatível com qualquer blockchain baseada em EVM (como Ethereum, Polygon, etc.) utilizando o padrão Proxy. Use as bibliotecas TransparentUpgradeableProxy, Initializable e TimelockController do OpenZeppelin.

O contrato atualizado deve incluir:

- Um atraso mínimo de 72 horas entre o momento em que um upgrade é iniciado e o momento em que ele entra em vigor, imposto pelo TimelockController.
- Um evento que é disparado no início do processo de atualização, contendo o endereço do novo contrato de implementação para verificação do usuário. Esse atraso tem o objetivo de permitir que os usuários validem as alterações antes de serem implementadas.
- Uma estrutura que permita a fácil adição e modificação de funções em versões futuras, preservando a estrutura das funções existentes. Ela também deve permitir a adição de novas variáveis de estado sem a necessidade de migração de dados.
- Uma convenção de nomenclatura para o contrato de implementação, que deve ser sufixado com V1. Esse contrato não deve ter conhecimento do proxy.

Requisitos:

- O código deve ser compilado sem espaços reservados, exceto para as funções do contrato original que não foram modificadas.
- Mantenha a solução o mais simples possível.
- Cada contrato deve ser escrito em seu próprio bloco de código. Esperamos três blocos de código, um para cada contrato.
- Forneça um script de implantação compatível com o Hardhat. Após a implantação, a função de administrador do TimeLockController deve ser renunciada.
- Tanto o proxy quanto os contratos herdados do TimeLockController devem ser mantidos básicos (apenas o construtor), pois todas as funcionalidades necessárias (upgradeTo, agendamento, execução) já estão incluídas no contrato básico. A maior parte do trabalho será feita no script de implantação e no script de atualização.
- O protótipo mais recente do construtor TimeLockController tem um quarto argumento adicional `TimelockController(uint256 minDelay, address[] memory proposers, address[] memory executors, address admin);`. O último argumento é o administrador. Para simplificar, coloque o administrador como o implantador (msg.sender). Também renuncie à função de administrador chamando renounceRole(DEFAULT_ADMIN_ROLE, msg.sender); dentro do construtor.
Aqui está o contrato inteligente: 
<código original do contrato inteligente>
Enter fullscreen mode Exit fullscreen mode

Um diagrama que explica as interações entre as diferentes partes:

Diagrama que explica a interação entre os diferentes contratos. As "72 horas" são o tempo que escolhemos, mas você pode escolher outros períodos.

Implantando o contrato no Remix

Você pode implantar o contrato usando a linha de comando ou um ambiente de desenvolvimento integrado (IDE) online, como o Remix. Se você estiver usando a linha de comando, pode gerar automaticamente o script de implantação com o ChatGPT. No prompt descrito na seção anterior, solicitamos ao ChatGPT que o gerasse para o Hardhat. Sinta-se à vontade para modificar a linha correspondente do prompt para solicitar outros tipos de scripts de implantação. Além disso, verifique a seção do Anexo para ver o resultado obtido.

É importante observar a ordem em que implantamos o contrato. Precisamos implantar tanto a implementação do contrato quanto o TimelockController antes de implantar o contrato Proxy. Isso é necessário, uma vez que os dois argumentos do contrato proxy são os endereços de implantação. Aqui está um diagrama representando a ordem.

Etapas de implantação

Por fim, é importante garantir que a função renounceRole() do TimeLockController seja chamada. Isso é importante para evitar que uma única parte tenha controle total sobre o contrato e para torná-lo descentralizado. No nosso caso, o ChatGPT colocou o renounceRole() no próprio construtor, portanto não é necessário chamá-lo na hora da implantação.

Nota: no Remix, ao implantar o proxy, o campo _DATA do construtor deve ser definido como "0x" para uma string vazia de bytes.

Atualizando o Contrato Inteligente

Atualizar um contrato envolve criar uma nova versão que introduza variáveis e funções adicionais, sem eliminar as existentes. Isso garante continuidade operacional e evita interrupções. Permite melhorias sem comprometer a compatibilidade ou exigir migração de dados.

Uma vez que o novo contrato tenha sido escrito, você precisará primeiro propô-lo usando a função schedule(). Em seguida, após o intervalo configurado (configurado na fase de implantação) tiver expirado, você pode chamar a função execute() para realmente implantar o novo contrato. Tanto schedule() quanto execute() fazem parte do TimeLockContract.

Aqui também, você pode pedir ao chatGPT para gerar o script necessário para executar e atualizar (consulte a seção Extra para os scripts que obtive). Pessoalmente, eu não cheguei tão longe a ponto de atualizar o contrato atual, pois não tinha novos recursos para adicionar. Vou atualizar este artigo no caso de acontecer algo assim.

Conclusão

E é isso aí! Tornar seus contratos inteligentes Solidity atualizáveis é um passo inestimável para garantir que seus projetos possam resistir ao teste do tempo. Se você é um desenvolvedor experiente em blockchain ou um novato no ramo, espero que este guia seja útil em seus futuros empreendimentos de desenvolvimento. Boa codificação!

Anexo

O script de implantação que eu usei:

const hre = require("hardhat");

async function main() {
  const [deployer, admin] = await hre.ethers.getSigners();
  console.log("Deployed by:", deployer.address, "admin", admin.address);

  const minDelay = 72 * 60 * 60; // 72 horas em segundos
  const proposers = [admin.address];
  const executors = [admin.address];

  const CheckClaimV1 = await ethers.getContractFactory("CheckClaimV1");
  const checkClaimV1 = await CheckClaimV1.deploy();
  await checkClaimV1.deployed();
  console.log("CheckClaimV1 deployed at:", checkClaimV1.address);

  const CheckClaimTimelock = await ethers.getContractFactory("CheckClaimTimelock");
  const timelock = await CheckClaimTimelock.deploy(minDelay, proposers, executors, admin.address);
  await timelock.deployed();
  console.log("CheckClaimTimelock deployed at:", timelock.address);

  const CheckClaimProxy = await ethers.getContractFactory("CheckClaimProxy");
  const proxy = await CheckClaimProxy.deploy(checkClaimV1.address, timelock.address, '0x');
  await proxy.deployed();
  console.log("CheckClaimProxy deployed at:", proxy.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
Enter fullscreen mode Exit fullscreen mode

Artigo escrito por TechExplorer. Traduzido por Marcelo Panegali.

Latest comments (0)