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
2 . Função de acesso
3 . Try
4 . Manipulação de call ou staticall
5 . Função de acesso
6 . 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];
}
}
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
}
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++;
}
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)