Uma das melhores características das blockchains é que os dados são imutáveis. Isso significa que ninguém pode mudar o conteúdo de qualquer uma das transações sem invalidar toda a blockchain.
Mas infelizmente, existem alguns problemas com esse sistema:
- Quando você implanta seu contrato inteligente, você não pode mudar o código mais tarde e assim não pode atualizá-lo. Uma transação é enviada e permanece para sempre, e não há nada que você possa fazer para reverter o estado da EVM.
- Se você quiser atualizar seu contrato inteligente, precisaria implantar um novo contrato em um novo endereço, pagar as taxas de gas, migrar todos os dados e convencer seus usuários a migrar para a versão 2 do contrato.
Esse é um processo bem desajeitado, toma muito tempo e tem uma grande chance de cometer algum erro na migração dos dados, o que pode levar a consequências desastrosas para os usuários.
Entretanto, a boa notícia é que: você pode contornar esse problema utilizando um contrato inteligente atualizável, e esse é o tópico principal deste artigo!
1. Como funciona?
O sistema é bem simples, você precisará de dois contratos inteligentess diferentes.
- O primeiro é chamado de implementação (ou a lógica), e ele contém todas as funções regulares do contrato inteligente. (como transfer(), approve() e assim por diante)
- O segundo é um contrato de proxy, que contém: um endereço de armazenamento que aponta para o endereço da implementação do contrato e uma função que delega todas as chamadas para a implementação do contrato e armazenamento do dApp.
Quando você chama (por exemplo) transfer em um contrato de proxy, o proxy vai “delegatecall()” transfer na implementação do contrato.
Se você não souber o que delegatecall é: https://solidity-by-example.org/delegatecall/
Isso significa que a função transfer() (inicialmente escrita no contrato de implementação) será executada no contexto do contrato de proxy. (A diferença de contratos regulares é que o armazenamento está situado no contrato de proxy mas a função no contrato de implementação).
É muito útil, pois quando você precisar atualizar um contrato inteligente:
Você só precisa configurar o endereço da implementação no contrato de proxy para o novo contrato de lógica/implementação.
Mas o armazenamento ainda está no proxy. Ele não está perdido (se armazenássemos os dados no contrato de implementação então quando fizéssemos a atualização para uma nova implementação do contrato, o armazenamento seria reiniciado).
E é isso! Quando todas essas operações forem feitas, você pode descartar o contrato inteligente anterior e começar a usar o novo como antes.
Qualquer usuário que chamar o contrato não verá nenhuma diferença além das funções atualizadas. Pronto! \
2. Existem 3 padrões de proxy diferentes
Existem diferentes maneiras de organizar contratos inteligentes organizáveis, usando sistemas mais ou menos complexos.
2.1 Padrão de Proxy Transparente
Esse é o padrão de proxy mais simples possível, e se comporta como descrito acima na primeira seção. Aqui está o código:
contract proxy{
address _impl; // endereço para implementação
function upgradeTo(address newImpl) external {
_impl = newImpl;
}
fallback() {
assembly {
let ptr := mload(0x40)
//(1) copia os dados vindos da chamada
calldatacopy(ptr, 0, calldatasize)
// (2) chamada para a frente para a lógica do contrato
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
let size := returndatasize
// (3) recupera os dados retornados
returndatacopy(ptr, 0, size)
// (4) retorna os dados de volta para o chamador
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
// e outras funções
}
Toda vez que um usuário chama uma função no proxy, a função fallback delega todos os parâmetros para o contrato de implementação e retorna o resultado. (reverte se o resultado for zero).
Se quisermos atualizar o contrato, apenas precisamos implantar o novo contrato inteligente e mudar a implementação no proxy para o contrato inteligente chamando a função upgradeTo que configura a implementação. (não esqueça de adicionar um dono do contrato, de outra maneira todo o mundo estará apto a atualizar o contrato com seu próprio código).
2.2 Proxy UUPS (ou proxy ERC1967)
Um terceiro contrato inteligente é adicionado: o contrato ProxyAdmin, que controla o proxy público. (O que delega para a implementação)
Ele contém algumas funções administrativas, para garantir que apenas um pequeno conjunto de endereços possam atualizar o proxy.
2.3 O terceiro é o Proxy diamante, que é bem mais complexo mas passa todas as limitações do modelo UUPS (que veremos daqui a pouco)
- O armazenamento e as funções são divididas em dois diferentes contratos (no mínimo).
- Você pode ter vários contratos para um contrato (para contornar o limite de 24kb).
Isso serve para aplicações grandes e descentralizadas, mas no nosso caso é bem complicado, então nas próximas seções estaremos mais focados nos proxies UUPS, que serve para 98% dos DApps.
3. Tutorial de proxy atualizável
Nesta seção, mostrarei como usar o proxy atualizável UUPS junto com o hardhat.
Estou assumindo que você já sabe como criar e implantar um contrato inteligente com essa ferramenta. Caso não, visite https://hardhat.org/
Existem 2 passos rápidos para a instalação:
-
Instalação
npm install -- save-dev @openzeppelin/hardhat-upgrades npm install -- save-dev @nomiclabs/hardhat-ethers ethers
-
Registrar o novo plugin no hardhat.config.js
require('@openzeppelin/hardhat-upgrades');
Agora para o exemplo, escreveremos um contrato inteligente de teste:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract MyContract {
function returnValue() public view returns(uint) {
return 1;
} }
O contrato contém apenas uma função que sempre retornará 1.
Nosso objetivo é atualizar esse contrato para sempre retornar 2.
São 2 passos.
1) Criar o script para implantar o proxy e a primeira versão do contrato (em scripts/deploy.js)
const { ethers, upgrades } = require("hardhat"); async function main() {
const Box = await ethers.getContractFactory("MyContract");
// pega o contrato MyContract que retorna 1
const box = await upgrades.deployProxy(Box);
// implanta o proxy e o contrato
await box.deployed();
console.log("Box deployed to:", box.address);
// mostra o endereço do contrato depois de ser implantado
}
Não esqueça de rodar scripts/deploy.js
npx hardhat run --network [your_network] scripts/deploy.js
2) Criar o script para atualizar o contrato inteligente (scripts/update.js)
const { ethers, upgrades } = require("hardhat");
let BOX_ADDRESS = "..."
// configura BOX_ADDRESS para box.address, o endereço do proxy
async function main() {
const BoxV2 = await ethers.getContractFactory("MyContractV2");
// pega o novo contrato (versão 2)
const box = await upgrades.upgradeProxy(BOX_ADDRESS, BoxV2);
// configura a implementação do proxy para BOXV2
console.log("Box upgraded");
// quando terminar, escreva box upgraded.
}
Aqui está o contrato MycontractV2, e a única diferença da primeira versão é que a função sempre retorna 2.
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract MyContract {
function returnValue() public view returns(uint) {
return 2;
}
}
E pronto, toda vez que você quiser atualizar seu contrato inteligente, execute o script update.js com o novo contrato na variável BOX_ADDRESS. (que precisa ser o endereço do proxy).
npx hardhat run -- testnet testnet scripts/update.js
O hardhat fará o resto. Escrever contratos atualizáveis nunca foi tão fácil!
Regras importantes.
Aviso: lembre-se de que usar proxy vem a certo custo. Na verdade, existem algumas limitações dos contratos atualizáveis UUPS
1) O construtor deve estar vazio
Em linguagens de programação, o construtor é sempre executado na primeira vez para iniciar os parâmetros, mas quando atualizamos contratos inteligentess, o construtor é executado outra vez, o que pode levar a comportamento indefinido. Precisamos garantir que todos os parâmetros sejam inicializados uma vez.
No lugar do construtor, usaremos uma função inicializadora com um modificador de inicialização.
// construtor perigoso
constructor(uint256 _x,_y) { x = _x y = _y }
// deve se tornar
function initialize(uint256 _x,uint256 _y) public initializer { x = _x y = _y } modifier initializer() {
require(!initialized, "Contract instance has already been initialized");
initialized = true; _;
}
Quando o inicializador é chamado uma vez, o modificador é marcado com: inicializado = true, e previne segundas chamadas à inicialização.
Info: quando você implantar o proxy e o contrato pela primeira vez, o hardhat chama initialize() diretamente, então você não precisa fazer isso manualmente.
2) Você não pode deletar, mudar o tipo e a ordem das variáveis.
Por quê? Pois isso irá reordenar os espaços no armazenamento EVM e deixar o contrato inutilizável. Exemplo:
uint a = 11
//slot 1
uint b = 22
//slot 2
uint c = 33
//slot 3
Se reordenarmos b e c e atualizarmos para uma versão 2, teremos:
uint a
// slot 1
uint c
// slot 2
uint b
// slot 3
O slot (espaço) 2 e o 3 não serão reordenados no proxy, onde os dados são armazenados. O espaço 2 será igual a 22 ao invés de 33, e c será igual a 22.
O espaço 3 será igual a 33 ao invés de 22, e b será igual a 33.
Problemas similares acontecem quando uma variável é deletada (os espaços na EVM não são liberados).
Na realidade, qualquer atualização que mexa na ordem do armazenamento EVM pode danificar os dados.
Isso é conhecido como storage clash vulnerability.
3) Você não pode utilizar bibliotecas externas
Essas limitações impõem que as bibliotecas não sejam compatíveis com contratos atualizáveis.
Para resolver esse problema, o Openzeppelin reescreveu muitas das suas bibliotecas para serem seguras de atualizar, mas devem seguir algumas regras.
Então, é possível escrever contratos ERC20, ERC721, ERC1155 atualizáveis.
Para isso, você terá que instalar adicionalmente: @openzeppelin/contracts-upgradable
Os contratos são escritos aqui: GitHub - OpenZeppelin/openzeppelin-contracts-upgradeable: Upgradeable variant of OpenZeppelin Contracts, meant for use in upgradeable contracts.
Outras limitações estão descritas aqui:
https://docs.openzeppelin.com/upgrades-plugins/1.x/faq
4. Conclusão
Espero que você tenha gostado desse tutorial sobre contratos inteligentess atualizáveis.
Esse artigo foi traduzido por Lorenzo Battistela e pode ser encontrado originalmente aqui.
Latest comments (0)