29 de setembro de 2022
Como eu mencionei em meu post anterior, o uso não adequado da função delegatecall pode levar a uma brecha de segurança. Um dos problemas é substituição acidental de variável, pois a delegatecall não utiliza o nome das variáveis, mas sua posição no armazenamento.
contract Proxy{
uint256 public var1;
uint256 public var2;
address public owner;
address public addressOfLogic;
constructor(address _addressOfLogic) {
addressOfLogic = _addressOfLogic
owner = msg.sender;
}
function upgradeTo(address _addressOfLogic) public {
require(msg.sender == owner, "Only Owner");
addressOfLogic = _addressOfLogic;
}
fallback() external payable {
assembly {
// código omitido para simplificar
}
}
}
contract Logic{
uint256 public var0;
uint256 public var1;
uint256 public var2;
function setVar1(uint256 _var1) public {
var1 = _var1;
}
function setVar2(uint256 _var2) public {
var2 = _var2;
}
}
Vejamos estes contratos. A variável addressOfLogic
contém o endereço da implementação atual do Logic. Apenas o owner
é capaz de alterar essa variável. O contrato Logic nos dá duas funções: definir o valor de var1
e var2
. Porém, quando usamos o Proxy, o resultado será diferente do que pensamos. Se chamarmos a função setVar1 com o argumento 5, então o valor do segundo elemento de armazenamento do Proxy será igual a 5, no nosso caso, será var2
. Portanto, se não formos cuidadosos, podemos acidentalmente alterar a variável errada.
Podemos notar, usando a segunda função implementada no Logic, o quão grande a consequência poderia ser da diferença entre armazenamentos no Proxy e no Logic. À primeira vista, ela é semelhante à função anterior. Porém, essa função altera o valor da variável no terceiro slot do armazenamento. No Logic é var2
, mas no Proxy é owner
. Essa é uma grande brecha de segurança do nosso Proxy. Alguém poderia usar setVar2 e definir seu próprio endereço como parâmetro. Através disso, ele será o proprietário do Proxy e, como resultado, será capaz de definir addressOfLogic
e implementar seu próprio Logic.
Como redimensionar nosso armazenamento?
Sabemos agora que o armazenamento do Logic e do Proxy tem que ser o mesmo, mas o que deveríamos fazer com o armazenamento quando atualizamos o Logic? Nosso novo Logic tem que herdar o armazenamento do Logic anterior, então a versão nova não pode alterar a estrutura do armazenamento, mas pode adicionar nova variável de estado nos próximos slots não ocupados no armazenamento, portanto, no final da variável existente. Vejamos nos exemplos. Se tivermos LogicV1,
contract LogicV1 {
uint256 public var1;
uint256 public var2;
}
não podemos atualizá-lo para o contrato LogicV2 dessa forma:
contract LogicV2 {
uint256 public var1;
}
Remover da var2
teoricamente é permitido, mas temos que lembrar que o valor da var2
ainda estará no segundo slot do armazenamento. Portanto, se criarmos LogicV3 (contrato que atualiza esse contrato LogicV2) com a nova variável, o valor da nova será a princípio igual ao valor de var2
.
contract LogicV2 {
uint256 public var2;
uint256 public var1;
}
Mude de lugar a var1
e a var2
:
contract LogicV2 {
uint256 public var1;
address public var2;
}
Mude o tipo da var2
:
contract LogicV2 {
uint256 public var1;
uint256 public var3;
uint256 public var2;
}
Adicione uma nova variável não no final das variáveis existentes.
A adição apropriada da nova variável na nova implementação parece:
contract LogicV2 {
uint256 public var1;
uint256 public var2;
uint256 public var3;
}
Se quisermos mudar o nome da variável, podemos fazê-lo. Mas é permitido apenas o nome dela, não o tipo. Portanto, este também é um novo contrato adequado:
contract LogicV2 {
uint256 public slot0;
uint256 public slot1;
uint256 public var3;
}
Atualização do contrato Logic que herda de outros contratos
Vamos assumir que nosso contrato Logic se pareça com:
contract Logic is A, B{
uint256 public var1;
uint256 public var2;
}
E os contratos A e B se parecem com:
contract A {
uint256 public A0;
}
contract B {
uint256 public B0;
}
Então, o armazenamento do nosso Logic é (storage[0] é o primeiro slot do armazenamento, storage[1] é o segundo e assim por diante):
storage[0] -> A0
storage[1] -> B0
storage[2] -> var1
storage[3] -> var2
É fácil prever que, se alguém adicionar uma nova variável A ou B, a ordem de armazenamento do Logic será diferente. Também, se escrevermos,
contract Logic is B, A{
uint256 public var1;
uint256 public var2;
}
o armazenamento de saída será diferente.
Se não tivermos certeza de que não precisaremos de mais variáveis no contrato dos pais, podemos reservar o armazenamento usando uma gap de armazenamento. Para fazer isso, declaramos um array fixo como este:
contract A {
uint256 public A0;
uint256[100] public __gap;
}
Se,no futuro, tivermos que adicionar uma nova variável, apenas escreveremos:
contract A {
uint256 public A0;
uint256 public A1;
uint256[99] public __gap;
}
O nome _ _ gap vem de uma convenção criada pelo OpenZeppelin. Quando falamos sobre o OpenZeppelin, eu utilizei informações para esta seção de um ótimo artigo deles.
A outra possibilidade
Outra opção é criar um contrato que conterá todas as variáveis de estado. E tanto o Logic quanto o Proxy herdarão dele. Graças a isso, temos certeza de que ambos têm exatamente a mesma ordem de armazenamento.
Resumo
Nos familiarizarmos com a ideia de padrão de armazenamento de proxy herdado. Vejamos a vantagem e a desvantagem disso.
Vantagem
- Nos dá a possibilidade de atualizar nosso contrato.
- É simplesmente método.
Desvantagem
- As novas versões precisam herdar o armazenamento que pode conter muitas variáveis de estado que eles não usam. Portanto, com o tempo, esse método pode ser caro em sua implementação.
- Todas as implementações do Logic se tornam fortemente acopladas a contratos de proxy específicos e não podem ser usadas por outros contratos de proxy que declarem diferentes variáveis de estado.
- As novas versões precisam herdar o armazenamento que pode conter muitas variáveis de estado que eles não usam. Portanto, com o tempo, esse método pode ser caro em sua implementação.
- Todas as implementações do Logic se tornam fortemente acopladas a contratos de proxy específicos e não podem ser usadas por outros contratos de proxy que declarem diferentes variáveis de estado.
Espero que você ache esse post útil. Se tiver alguma ideia de como poderia melhorar meus posts, entre em contato. Estou sempre pronto para aprender. Você pode se conectar comigo no LinkedIn e no Telegram.
Se você quiser conversar comigo sobre este ou qualquer outro tópico que escrevi, sinta-se à vontade. Estou aberto para conversar.
Bom aprendizado!
Esse artigo foi escrito por Eszymi e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.
Top comments (0)