A necessidade de capacidade de atualização e as abordagens para alcançá-la em contratos inteligentes.
07 de julho de 2023
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
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;
}
}
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);
}
}
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();
}
}
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:
- 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.
- 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.
- 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.
- 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:
- Use modificadores de função externa: considere marcar funções como
external
em vez depublic
ouprivate
, 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. - 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.
- 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.
- 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.
- 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.
- 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.
- 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
- Instale o Hardhat e os plugins necessários executando:
npm i --save-dev hardhat @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades dotenv
- Crie um projeto Hardhat executando o seguinte comando e selecionando suas opções preferidas:
npx hardhat
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;
}
}
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);
});
- Configure seu arquivo .env.
PRIVATE_KEY=
ETHERSCAN_API_KEY=
POLYSCAN_API_KEY=
MUMBAI_ALCHEMY_API=
SEPOLIA_ALCHEMY_API=
MAINNET_ALCHEMY_API=
DEPLOY_NETWORK=
- 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,
},
},
};
-
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: [],
});
});
- Execute a tarefa de verificação:
npx hardhat verify:my-contract --network sepolia
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;
}
}
- 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');
});
- 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
- 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.
Esse artigo foi escrito por Dhrumil Dalwadi e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.
Top comments (0)