WEB3DEV

Cover image for Try-catch em Solidity: aqui está como e por quê
Isabela Curado Nehme
Isabela Curado Nehme

Posted on • Atualizado em

Try-catch em Solidity: aqui está como e por quê

22 de janeiro de 2023

Lidar com chamadas de funções externas no Solidity não é uma ciência de foguetes!


Sumário

..... . Detecte erros e desvie de reversões

1 . Bloqueio try catch

2 . Função de acesso

3 . Try

4 . Manipulação de call ou staticall

5 . Função de acesso

6 . Uso

7 . Outros casos de uso

8 . Conclusão


Detecte erros e desvie de reversões

O estado inicial de uma chamada de função é restaurado se encontrar erros de tempo de execução, função de reversão codificada rigidamente (hardcode) ou falha em uma instrução “require”. Isso pode ser um problema se for essencial modificar o estado de um contrato (ou estado de EVM (Ethereum Virtual Machine ou máquina virtual da Ethereum) global) independentemente do sucesso de uma chamada externa. Imagine um caso onde o usuário deseja fazer chamadas externas para um contrato de registro seguro (senha) e recuperar seus endereços através de um contrato inteligente E ele deseja contar as tentativas de acesso - bem-sucedidas ou não.

contract Ledger {
   address[2] users = [0x5B3...dC4, 0xAb8...cb2];
   uint constant key = 123;

   function getUser(uint _key, uint id) external view returns(address) {
       require(_key == key, "access denied");
       return users[id];
   }
}
Enter fullscreen mode Exit fullscreen mode

Bloqueio try catch

Função de acesso
function accessLedger(uint key, uint id) external {
       try Ledger(ledger).getUser(key,id) // chamada externa para o contrato de registro (Ledger)
       returns (address user){
           fetchedUser = user; // salva o usuário
       } catch Error(string memory data){
           emit FailureStr(data);
           latestError = "Error";
       } catch Panic(uint code) {
           emit Failure(abi.encodePacked(code));
           latestError = "Panic";
       } catch (bytes memory data){
           emit Failure(data);
           latestError = "Unnamed";
       }
       attempts++; // variável de unidade de estado para contar tentativas
   }
Enter fullscreen mode Exit fullscreen mode
Try

Seguindo a sintaxe, tentamos fazer uma chamada externa (chamadas de funções internas não são suportadas) para a função getUser() de um contrato Ledger que retorna o valor do endereço.

Se a chamada for bem-sucedida, vamos para o corpo do bloco try{}, evitando todos os blocos catch{} à frente (deve haver pelo menos um bloco catch{}). Podemos atribuir um valor de retorno a uma variável do tipo correspondente e manipulá-lo no corpo do bloco try{} como quisermos - nesse caso em particular, escrevemos o valor no estado de um contrato.

Catch

A parte mais interessante deste artigo - o bloco catch{}. Se uma chamada externa desejada resultar em um erro, podemos lidar com o erro dependendo do seu tipo e isso não suspenderá o código da função mais adiante. Blocos de tipos de erro catch{}:

  • Error(string memory data){} ou Erro(dados de string em memória){} - esse bloco é executado se a chamada for revertida com um esclarecimento de string (string clarification) (o estado require() falhou ou a função revert() foi chamada). Portanto, emitimos um evento FailureStr() e podemos ouvi-lo para depurar uma chamada com falha.
  • Panic(uint code){} ou Pânico(código uint){} - o bloco de pânico é executado no caso de erros do tipo assert() ou falhas graves, como divisão por zero ou tentativa de acesso a um dos membros de array inexistente. Lista completa de erros de pânico e códigos de erro.
  • catch(bytes memory data){} ou captura(dados de memória bytes){} - se a mensagem de erro não for decodificável para um tipo conhecido, esse bloco prossegue. Este pode ser o caso se uma chamada for revertida com um erro personalizado, portanto, os dados do erro serão salvos na variável bytes memory (memórias bytes).
  • Podemos usar o simples catch{} se não formos manipular os erros de forma alguma.

Essa abordagem nos habilita a lidar com falhas nas chamadas externas e fornecer feedback razoável sem interromper a execução da função.

Manipulação de call ou staticall

Função de acesso
function callLedger(uint key, uint id) external {
       (bool success, bytes memory data) =  ledger.staticcall(abi.encodeWithSignature("getUser(uint256,uint256)", key, id));
       if (!success) emit Failure(data);
       else fetchedUser = address(bytes20(uint160(uint(bytes32(data)))));
       attempts++;
   }
Enter fullscreen mode Exit fullscreen mode
Uso

Devido ao fato de que uma falha de call/staticall também não reverte toda a chamada de função inicial definindo apenas a variável bool de sucesso como false (falsa), essa abordagem também permite que o desenvolvedor lide com erros de alguma forma.

Se a chamada externa foi revertida por qualquer razão - a variável bytes memory conterá uma mensagem de erro que podemos manipular, emitindo-a usando nosso evento Failure(). No entanto, se a chamada tiver sucesso, o valor de retorno também será colocado em uma variável do tipo bytes memory, por isso, também devemos pensar em convertê-la para um tipo apropriado.

Tal abordagem demanda aproximadamente 1,5% mais gas em testes realizados do que sua amiga try-catch.

Outros casos de uso

Abraçando o fato de que a chamada externa também pode ser executada para contratos com uma palavra-chave this, então, podemos usar as vantagens de chamar funções externas usando try-catch dentro de um único contrato. Isso pode fazer sentido no caso da implementação de tokens ERC-***: o contrato se torna o próprio operador de token e todas as transações podem ser realizadas por meio de chamada externa para um contrato this - para que ele nos permita detectar quaisquer erros de transferência de token.

A abordagem try-catch pode ser aplicada à transações de criação de contratos. Isso também inclui a criação de contratos do tipo salted (contratos “salgados”) - usando o código de operação (opcode) CREATE2.

Conclusão

Try-catch é uma abordagem bem útil para lidar com erros de chamada externa, o que permite um feedback mais natural e conveniente do usuário, juntamente com sua eficiência de gas e segurança. Mas, lembre-se: às vezes, é mais vantajoso deixar a chamada reverter e economizar uma taxa de gas do que pagar por uma transação que está fadada ao fracasso.

Todos os casos de uso e código de contrato completo (explicado) podem ser encontrados aqui.

Esse artigo foi escrito por Andrei Samokish e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Latest comments (0)