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);
}
}
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 dedepositCheck
ewithdraw
. 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, ...
}
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) {
}
}
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);
}
}
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>
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);
});
Artigo escrito por TechExplorer. Traduzido por Marcelo Panegali.
Latest comments (0)