WEB3DEV

Cover image for Em proxies — Desafio Ethernaut de Call Delegate
Panegali
Panegali

Posted on

Em proxies — Desafio Ethernaut de Call Delegate

Neste artigo, abordarei a solução para o Ethernaut Challenge 6, bem como me aprofundarei na delegateCall e como podemos implementá-la na produção com padrões diferentes.

Ideia chave:

  • delegate call
  • msg.data

Delegate call

  • Quando um contrato faz uma chamada de função usando delegatecall, ele carrega o código da função de outro contrato e o executa como se fosse seu próprio código.
  • Quando uma função é executada com delegatecall, esses valores não mudam:

    address(this)
    msg.sender
    msg.value
    
  • Leituras e gravações em variáveis ​​de estado acontecem no contrato que carrega e executa funções com delegatecall. Leituras e gravações nunca acontecem no contrato que contém as funções que são recuperadas.

  • Portanto, se ContractA usa delegatecall para executar uma função de ContractB, os dois pontos a seguir são verdadeiros:

  • As variáveis ​​de estado em ContractA podem ser lidas e escritas.

  • As variáveis ​​de estado em ContractB nunca são lidas ou gravadas.

Solução

  • Para este problema, o address(delegate).delegatecall(msg.data)chama um contrato do tipo delegado
  • Como sabemos, a chamada do delegado pode alterar o estado do contrato chamando-o
  • Também é conveniente que tenhamos um contrato pwn() para controlar o proprietário do contrato
  • Portanto, tudo o que precisamos fazer é chamar a função de fallback para que a chamada do delegado seja chamada pwn()como msg.data

Então, o que é msg.data neste caso?

  • msg.data é a versão codificada em bytecode da assinatura da função

Abordagem

  1. Obtenha o código de bytes para pwn()
  2. Faça uma chamada com assinatura de função codificada em bytecode para o contrato que acionará a função de fallback

Em Javascript:

const encodedSignature = web3.eth.abi.encodeFunctionSignature("pwn()")
await web3.eth.sendTransaction({from: player, to: contract.address, data: encodedSignature})
Enter fullscreen mode Exit fullscreen mode

Em Solidity

Fazer hash com keccak256 e, em seguida, convertê-lo em bytes4 é exatamente como o Solidity gera assinaturas de função, então isso deve funcionar.

bytes4 encodedSignature = bytes4(keccak256("pwn()"));
Enter fullscreen mode Exit fullscreen mode

Ou

bytes4 encodedSignature = Delegate(0).pwn.selector
Enter fullscreen mode Exit fullscreen mode

Seguido por

(bool success, ) = address(contract.address).call(encodedSignature);
Enter fullscreen mode Exit fullscreen mode

Como usar delegate call com segurança

1. Controle o que é executado com delegatecall

  • Use permissões ou autenticações ou alguma outra forma de controle para especificar ou alterar quais funções e contratos são executados com delegatecall.
  • Não execute código não confiável com delegatecall porque ele pode modificar variáveis ​​de estado maliciosamente ou chamar selfdestruct para destruir o contrato de chamada.

2. Chamar delegatecall apenas em endereços que tenham código

  • Delegatecall retornará 'True' para o valor do estado se for chamado em um endereço que não seja um contrato e, portanto, não tenha código.
  • Verifique o endereço se não tiver certeza

1

código para verificar endereço

3. Gerenciar layout de variável de estado

  • O Solidity armazena dados em contratos usando um espaço de endereço numérico. A primeira variável de estado é armazenada na posição 0, a próxima variável de estado é armazenada na posição 1, a próxima variável de estado é armazenada na posição 2, etc.
  • Um contrato e uma função executados com delegatecall compartilham o mesmo espaço de endereço de variável de estado que o contrato de chamada, porque as funções chamadas com delegatecall leem e gravam as variáveis ​​de estado do contrato de chamada.
  • Portanto, um contrato e uma função chamados com delegatecall devem ter o mesmo layout de variável de estado para locais de variável de estado que são lidos e gravados. Ter o mesmo layout de variável de estado significa que as mesmas variáveis ​​de estado são declaradas na mesma ordem em ambos os contratos.
  • Se um contrato que chama delegatecall e o contrato com as funções emprestadas não tiverem o mesmo layout de variável de estado e eles lerem ou gravarem nos mesmos locais no armazenamento do contrato, eles substituirão ou interpretarão incorretamente as variáveis ​​de estado um do outro.
  • Por exemplo, digamos que um ContractA declare variáveis ​​de estado 'uint first;' e 'bytes32 segundos'; e ContractB declara variáveis ​​de estado 'uint first;' e 'nome da string';. Eles têm diferentes variáveis ​​de estado na posição 1 ('bytes32 segundo' e 'nome da string') no armazenamento do contrato e, portanto, eles escreverão e lerão dados errados entre eles na posição 1 se o delegado for usado entre eles.

Gerenciar o layout da variável de estado dos contratos que chamam funções com delegatecall e os contratos que são executados com delegatecall não é difícil de fazer na prática quando uma estratégia é usada para fazê-lo. Aqui estão algumas estratégias conhecidas que foram usadas com sucesso na produção:

Estratégias usadas na produção

Armazenamento Herdado (Inherited Storage)

  • Uma estratégia é criar um contrato que declare todas as variáveis ​​de estado usadas por todos os contratos que compartilham um espaço de endereço de armazenamento de contrato porque eles usam a chamada delegada entre eles.

Limitações

  • Uma limitação que o armazenamento herdado tem é que ele impede que os contratos sejam reutilizáveis. Se você implantar um contrato que usa armazenamento herdado, provavelmente não poderá reutilizar esse contrato implantado com diferentes contratos que possuem diferentes variáveis ​​de estado ao usar o delegatecall.
  • Outra limitação é que é muito fácil nomear acidentalmente algo como uma função interna ou variável local com o mesmo nome de uma variável de estado e ter um conflito de nomes. Mas isso pode ser superado usando convenções de nomenclatura de código que evitam tais conflitos de nome.

Apps Storage (Armazenamento de aplicativos)

Semelhante ao armazenamento herdado, ele resolve o problema de conflito de nomes, onde é muito fácil nomear acidentalmente algo como uma função interna ou variável local com o mesmo nome de uma variável de estado.

O AppStorage impõe uma convenção de nomenclatura ou acesso que torna impossível confrontar nomes de variáveis ​​de estado com outra coisa.

  • Uma estrutura chamada AppStorage é escrita em um arquivo Solidity.
  • A estrutura AppStorage contém variáveis ​​de estado que serão compartilhadas entre os contratos.
  • Para usá-lo, um contrato importa a estrutura AppStorage e declara AppStorage internal s; como a primeira e única variável de estado no contrato. O contrato acessa todas as variáveis ​​de estado em funções por meio da estrutura como esta: s.myFirstVariable, s.mySecondVariable, etc.

Aqui está um exemplo

2

Exemplo de armazenamento de aplicativos

  • É importante que 'AppStorage internal s'; é declarada como a primeira e única variável de estado em todos os contratos que a utilizam.
  • Isso o coloca na posição 0 no espaço de endereço de armazenamento. Portanto, se todos os contratos a declararem como a primeira e única variável de estado, os dados de armazenamento entre os contratos que usam delegatecall serão alinhados corretamente.
  • Não adicione variáveis ​​de estado diretamente em um contrato porque isso entrará em conflito com as variáveis ​​de estado declaradas na estrutura AppStorage. Para adicionar mais variáveis ​​de estado, adicione-as ao final da estrutura AppStorage ou use Diamond Storage.

Vantagens

  • Distingue o código de uma maneira que o torna mais fácil de escanear e ler. Se você se preocupa com a legibilidade do código, vai gostar do AppStorage.
  • O AppStorage é mais conveniente de usar do que o Diamond Storage porque, em todas as funções, o Diamond Storage requer a obtenção de um apontador para uma estrutura, enquanto com o AppStorage o apontador s da estrutura está automaticamente disponível ao longo de um contrato.
  • Outra vantagem que o AppStorage tem sobre o Inherited Storage é que o AppStorage pode ser acessado pelas bibliotecas do Solidity da mesma forma que o Diamond Storage. Uma struct (registro) AppStorage é sempre armazenada no local 0 para que as funções internas nas bibliotecas do Solidity possam usar isso para inicializar o apontador de armazenamento 's' para apontar para a struct AppStorage. Aqui está um exemplo disso:
  • O AppStorage é particularmente útil para contratos específicos de aplicativos ou projetos que não serão reutilizados com outros projetos ou contratos que também usam AppStorage ou armazenamento herdado. AppStorage pode ser usado com Diamond Storage no mesmo contrato.
  • AppStorage pode ser usado com herança de contrato. Isso é feito declarando 'AppStorage internal s;' em um contrato. Em seguida, todos os contratos que usam AppStorage herdam esse contrato.

Leituras

Diamond storage

  • Este é um padrão popular como visto em eip2535
  • Os contratos que usam a delegatecall entre eles não precisam declarar as mesmas variáveis ​​de estado na mesma ordem se armazenarem dados em locais diferentes.
  • Conforme mencionado anteriormente, o Solidity armazena automaticamente as variáveis ​​de estado em locais de armazenamento começando em 0 e incrementando em um. Mas não precisamos usar o mecanismo de layout de armazenamento padrão do Solidity. Não precisamos armazenar dados começando no local 0.
  • Podemos especificar onde começar a armazenar dados no espaço de endereço. Para diferentes contratos, podemos especificar diferentes locais para começar a armazenar dados, evitando assim que diferentes contratos com diferentes variáveis ​​de estado entrem em conflito com os locais de armazenamento.
  • Podemos fazer o hash de uma string única para obter uma posição de armazenamento aleatória e armazenar uma struct lá. A struct pode conter todas as variáveis ​​de estado que queremos. A string exclusiva pode atuar como um namespace para uma funcionalidade específica.
  • Por exemplo, poderíamos implementar um contrato ERC721. Este contrato poderia armazenar uma estrutura chamada 'ERC721Storage' na posição 'keccak256(“com.myproject.erc721”);'. A struct pode conter todas as variáveis ​​de estado relacionadas à funcionalidade ERC721 que o contrato ERC721 lê e grava.

Vantagens

  • Uma delas é que o contrato ERC721 é reutilizável.
  • Outra boa vantagem do Diamond Storage é que é possível que as funções internas das bibliotecas do Solidity acessem o Diamond Storage como qualquer função de contrato regular.

Leituras recomendadas


Artigo escrito por Yong Kang Chia e traduzido por Marcelo Panegali

Latest comments (0)