Eu tenho revisado solidity recentemente para consolidar alguns detalhes e escrever um "Guia Simplificado para Solidity" para iniciantes (programadores experientes podem procurar outras referências). Vou atualizar o guia com 1-3 lições por semana.
Twitter: @0xAA_Science
Comunidade: Discord | Grupo do WhatsApp | Site Oficial wtf.academy
Todo o código e tutoriais estão disponíveis no GitHub: github.com/AmazingAng/WTF-Solidity
Nesta lição, vamos falar sobre Contratos de Proxy. O código de ensino é uma versão simplificada do contrato de Proxy do OpenZeppelin.
Os contratos Solidity
são imutáveis após serem implantados na cadeia. Isso tem suas vantagens e desvantagens:
- Vantagens: segurança, os usuários sabem o que esperar (na maioria das vezes).
- Desvantagens: mesmo que haja um bug no contrato, não é possível modificá-lo ou atualizá-lo, apenas implantar um novo contrato. Além disso, o novo contrato terá um endereço diferente do anterior, e a migração dos dados do contrato existente para o novo exigirá um alto consumo de gas.
Existe uma maneira de modificar ou atualizar contratos após a implantação? Sim, através do modo de proxy.
No modo de proxy, os dados e a lógica do contrato são separados, armazenados em contratos diferentes. Usando o simples contrato de proxy mostrado no diagrama acima como exemplo, os dados (variáveis de estado) são armazenados no contrato de proxy, enquanto a lógica (funções) é armazenada em outro contrato de lógica. O contrato de proxy delega toda a chamada de função para o contrato de lógica usando delegatecall
e depois retorna o resultado final ao chamador.
O modo de proxy tem duas principais vantagens:
- Atualização: quando precisamos atualizar a lógica do contrato, basta direcionar o contrato de proxy para o novo contrato de lógica.
- Economia de gas: se vários contratos reutilizarem a mesma lógica, basta implantar um contrato de lógica e, em seguida, implantar vários contratos de proxy que armazenam apenas os dados e se conectam à lógica central.
Dica: Se você não está familiarizado com o delegatecall
, pode conferir a Lição 23 do tutorial.
Aqui está um contrato de Proxy simples, simplificado a partir do contrato de Proxy do OpenZeppelin. Ele consiste em três partes: Contrato de Proxy Proxy
, Contrato de Lógica Logic
e um exemplo de chamada Caller
. O código é simples:
- Implante o contrato de lógica
Logic
primeiro. - Crie o contrato de proxy
Proxy
, onde a variável de estadoimplementation
registra o endereço do contratoLogic
. - O contrato
Proxy
usa a função de callbackfallback
para delegar todas as chamadas ao contratoLogic
. - Por fim, implante o contrato de chamada
Caller
e chame o contrato de Proxy.
O contrato de Proxy
é curto, mas usa linguagem de montagem inline, o que pode tornar o entendimento um pouco mais desafiador. Possui apenas uma variável de estado, um construtor e uma função de fallback. A variável de estado implementation
é inicializada no construtor e é usada para armazenar o endereço do contrato Logic
.
contract Proxy {
address public implementation; // endereço do contrato de lógica
/**
* @dev Inicializa o endereço do contrato de lógica
*/
constructor(address implementation_){
implementation = implementation_;
}
A função de fallback do Proxy
encaminha todas as chamadas externas para o contrato Logic
usando delegatecall
. Esta função de fallback é única, pois permite a devolução de valores mesmo sem um valor de retorno padrão. Ela usa operações de montagem inline como calldatacopy
, delegatecall
, returndatacopy
e outras para realizar a ação corretamente.
/**
* @dev Função de fallback, delega a chamada desse contrato para o contrato `implementation`
* Usa montagem para permitir o retorno de valores mesmo sem um valor de retorno padrão
*/
fallback() external payable {
address _implementation = implementation;
assembly {
// Copia calldata para a memória
calldatacopy(0, 0, calldatasize())
// Chama o contrato 'implementation' por meio do delegatecall
let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
// Copia o retorno para a memória
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
Este é um contrato de lógica muito simples, criado apenas para fins de demonstração do contrato de Proxy. Ele contém 2
variáveis, 1
evento e 1
função:
implementation
: variável de espaço reservado, mantida consistente com o contrato deProxy
para evitar conflitos de slots.x
: variáveluint
definida como99
.CallSuccess
: evento acionado quando a chamada é bem-sucedida.increment()
: função que será chamada pelo contrato deProxy
, acionando o eventoCallSuccess
, e retornando umuint
, cujo selecionador é0xd09de08a
. Quando chamada diretamente, a função retornaria100
, mas chamada através doProxy
retornará1
.
/**
* @dev Contrato de lógica para executar as chamadas delegadas
*/
contract Logic {
address public implementation; // mantido consistente com Proxy para evitar conflitos de slots
uint public x = 99;
event CallSuccess();
// Esta função aciona o evento CallSuccess e retorna um uint
// Selector da função: 0xd09de08a
function increment() external returns(uint) {
emit CallSuccess();
return x + 1;
}
}
O contrato Caller
demonstra como chamar um contrato de proxy. É um contrato simples que precisa que você entenda as lições sobre call
e ABI encoding
.
Possui 1
variável e 2
funções:
proxy
: variável de estado que armazena o endereço do contrato de proxy.- Construtor: inicializa a variável
proxy
ao implantar o contrato. increase()
: chama a funçãoincrement()
do contrato de proxy usandocall
e retorna umuint
. Para realizar a chamada, usamosabi.encodeWithSignature()
para obter o seletor da funçãoincrement()
, e para decodificar o valor de retorno, usamosabi.decode()
.
/**
* @dev Contrato Caller que chama o contrato de Proxy e obtém o resultado
*/
contract Caller{
address public proxy; // endereço do contrato de proxy
constructor(address proxy_){
proxy = proxy_;
}
// Chama a função increment() através do contrato de Proxy
function increment() external returns(uint) {
( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()"));
return abi.decode(data,(uint));
}
}
-
Implante o contrato de
Logic
. -
Chame a função
increment()
do contrato deLogic
, que retorna100
. -
Implante o contrato de
Proxy
e forneça o endereço do contrato deLogic
. -
Chame a função
increment()
do contrato deProxy
, sem retorno. -
Implante o contrato
Caller
e forneça o endereço do contrato deProxy
. -
Chame a função
increment()
do contratoCaller
, que retornará1
.
Nesta lição, apresentamos o modo de proxy e um contrato de proxy simples. O contrato de proxy utiliza a função delegatecall
para delegar chamadas de função para outro contrato de lógica, separando assim os dados e a lógica em contratos diferentes. Além disso, ele utiliza operações de montagem inline para permitir que a função de fallback, que normalmente não teria um valor de retorno, retorne dados. A pergunta que deixamos para você foi: por que chamar increment()
através do Proxy retornará 1
? De acordo com a Lição 23 sobre delegatecall, ao chamar uma função do contrato de lógica através do contrato de proxy, qualquer operação que modifique ou leia variáveis de estado no contrato de lógica afetará as variáveis de estado correspondentes no contrato de proxy. Como a variável x
do contrato de proxy não foi definida (ou seja, corresponde ao zero na posição de armazenamento do contrato de proxy), chamar increment()
através do Proxy retornará 1
.
Na próxima lição, veremos contratos de proxy atualizáveis.
Embora os contratos de proxy sejam poderosos, eles também são propensos a bugs, então é recomendável copiar os modelos de contratos do OpenZeppelin.