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,
Logic1
eLogic2
, respectivamente. -
Implemente o contrato de proxy transparente
TransparentProxy
e aponte o endereço deimplementation
para o contrato de lógica antigo. -
Utilizando o seletor
0xc2985578
, chame a funçãofoo()
do contrato de lógica antigoLogic1
no 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 antigoLogic1
no contrato de proxy. A chamada será bem-sucedida e a variávelwords
será alterada para"old"
. -
Troque de volta para a carteira do administrador, chame a função
upgrade()
e aponte o endereço deimplementation
para 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ógicaLogic2
no contrato de proxy. A variávelwords
será 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.