Recentemente, tenho revisado meus conhecimentos em solidity, reforçando os detalhes e escrevendo um "WTF Introdução Simples ao Solidity" para iniciantes (programadores experientes devem procurar outros tutoriais). Atualizo de 1 a 3 lições por semana.
Twitter: @0xAA_Science
Comunidade: Discord | Grupo no WeChat | Website wtf.academy
Todo o código e tutoriais são de código aberto no GitHub: github.com/AmazingAng/WTF-Solidity
Nesta lição, vamos falar sobre o conflito de seletores em contratos de proxy e como resolver esse problema com o Proxy Transparente. O código educacional foi simplificado a partir do TransparentUpgradeableProxy do OpenZeppelin e não deve ser usado em produção.
Em contratos inteligentes, o seletor de uma função (selector) são os primeiros 4 bytes do hash da assinatura da função. Por exemplo, o seletor de mint(address account) é bytes4(keccak256("mint(address)")), que é 0x6a627842. Para mais informações sobre seletores, veja a Aula 29 do WTF Solidity: Seletores de Funções.
Devido ao fato dos seletores serem compostos por apenas 4 bytes, é possível que duas funções diferentes tenham o mesmo seletor. Por exemplo, as funções a seguir:
// Exemplo de conflito de seletores
contract Foo {
function burn(uint256) external {}
function collate_propagate_storage(bytes16) external {}
}No exemplo acima, as funções burn() e collate_propagate_storage() têm o mesmo seletor 0x42966c68, o que é um conflito de seletores. Nesse caso, o EVM não consegue distinguir qual função o usuário está tentando chamar, impossibilitando a compilação do contrato.
Mesmo que exista um conflito de seletores entre um contrato de lógica e um contrato de proxy, ainda é possível compilar o código. No entanto, isso pode levar a sérios problemas de segurança. Por exemplo, se a função a do contrato de lógica tiver o mesmo seletor que a função de atualização do contrato de proxy, o administrador poderá inadvertidamente transformar o contrato em um buraco negro ao chamar a função a. As consequências seriam catastróficas.
Atualmente, existem dois padrões de contratos atualizáveis que resolvem esse problema: Proxy Transparente e UUPS (Universal Upgradeable Proxy System).
A lógica por trás de um Proxy Transparente é muito simples: o administrador pode, por causa de um "conflito de seletores", chamar acidentalmente a função de atualização do contrato de proxy ao tentar chamar uma função do contrato de lógica. Para evitar isso, a solução é restringir os poderes do administrador da seguinte maneira:
- O administrador se torna um executor de tarefas e pode chamar apenas as funções de atualização do contrato de proxy para mudanças, sem poder chamar funções de chamada de volta para o contrato de lógica.
- Os demais usuários não conseguem chamar as funções de atualização do contrato, mas podem chamar as funções do contrato de lógica.
O contrato de Proxy deste exemplo é muito semelhante ao da 47ª aula, com a diferença de que a função fallback() agora tem uma verificação adicional para evitar que o administrador chame as funções da lógica.
Ele contém 3 variáveis:
implementation: endereço do contrato de lógica.admin: endereço do administrador.words: uma string que pode ser alterada pelas funções do contrato de lógica.
Este contrato contém 3 funções:
- Construtor: inicializa o administrador e o endereço do contrato de lógica.
fallback(): função de chamada de volta que delega a chamada para o contrato de lógica, mas não pode ser chamada pelo administrador.upgrade(): função de atualização que altera o endereço do contrato de lógica, só pode ser chamada pelo administrador.
// Código de exemplo de um contrato de proxy transparente, não use em produção.
contract TransparentProxy {
address implementation; // endereço do contrato de lógica
address admin; // administrador
string public words; // uma string que pode ser alterada pelas funções do contrato de lógica
// Construtor, inicializa o administrador e o endereço do contrato de lógica
constructor(address _implementation){
admin = msg.sender;
implementation = _implementation;
}
// Função de chamada de volta, delega a chamada para o contrato de lógica
// Não pode ser chamada pelo administrador para evitar conflitos de seletores
fallback() external payable {
require(msg.sender != admin);
(bool success, bytes memory data) = implementation.delegatecall(msg.data);
}
// Função de atualização, altera o endereço do contrato de lógica. Só pode ser chamada pelo administrador
function upgrade(address newImplementation) external {
if (msg.sender != admin) revert();
implementation = newImplementation;
}
}Os contratos de lógica novo e antigo são idênticos à 47ª aula. Eles contêm 3 variáveis de estado para manter a consistência com o contrato de proxy e uma função foo(). O contrato antigo altera o valor de words para "old", enquanto o novo altera para "new".
// Contrato de lógica antigo
contract Logic1 {
// Variáveis de estado que devem ser compatíveis com o contrato de proxy para evitar a colisão de slots
address public implementation;
address public admin;
string public words; // uma string que pode ser alterada pelas funções do contrato de lógica
// Altera as variáveis de estado do contrato de proxy, seletor: 0xc2985578
function foo() public{
words = "old";
}
}
// Contrato de lógica novo
contract Logic2 {
// Variáveis de estado que devem ser compatíveis com o contrato de proxy para evitar a colisão de slots
address public implementation;
address public admin;
string public words; // uma string que pode ser alterada pelas funções do contrato de lógica
// Altera as variáveis de estado do contrato de proxy, seletor: 0xc2985578
function foo() public{
words = "new";
}
}-
Implemente os contratos de lógica antigo e novo,
Logic1eLogic2, respectivamente.

-
Implemente o contrato de proxy transparente
TransparentProxye aponte o endereço deimplementationpara o contrato de lógica antigo.
-
Utilizando o seletor
0xc2985578, chame a funçãofoo()do contrato de lógica antigoLogic1no contrato de proxy. A chamada falhará, pois o administrador não pode chamar funções de lógica.
-
Troque para uma nova carteira e, usando o seletor
0xc2985578, chame a funçãofoo()do contrato de lógica antigoLogic1no contrato de proxy. A chamada será bem-sucedida e a variávelwordsserá alterada para"old".
-
Troque de volta para a carteira do administrador, chame a função
upgrade()e aponte o endereço deimplementationpara o novo contrato de lógicaLogic2.
-
Troque para uma nova carteira e, utilizando o seletor
0xc2985578, chame a funçãofoo()do novo contrato de lógicaLogic2no contrato de proxy. A variávelwordsserá alterada para"new".
Nesta lição, explicamos o conflito de seletores em contratos de proxy e como evitar esse problema com um Proxy Transparente. O Proxy Transparente resolve esse problema limitando as ações do administrador. Embora essa solução gere um custo adicional de gás a cada chamada de função pelos usuários, o Proxy Transparente ainda é a escolha favorita da maioria dos projetos.
Na próxima lição, abordaremos o sistema de proxy universal de atualização (UUPS), que é mais complexo, porém exige menos gás.
