WEB3DEV

Cover image for 3 Métodos Alternativos Seguros para Substituir o tx.origin por desenvolvedores de Solidity
Fatima Lima
Fatima Lima

Posted on

3 Métodos Alternativos Seguros para Substituir o tx.origin por desenvolvedores de Solidity

Image description

O que é tx.origin?

tx.origin é uma variável global nos contratos inteligentes da Ethereum que representa o endereço externo original que iniciou a transação. Basicamente, é o endereço da carteira que usamos todos os dias!

Ele refere-se ao endereço que originalmente criou e assinou uma transação, independentemente de por quantos contratos a transação tenha passado antes de atingir seu estado atual, ou seja, onde a variável tx.origin está sendo usada no código.

É extremamente tentador usar o tx.origin no contrato inteligente, já que ela pode realmente simplificar a vida dos desenvolvedores. Mas, se você conversou com algum desenvolvedor sênior de contratos inteligentes, com certeza viu que ele é exigente quanto ao seu uso no código. Por quê?

Neste artigo, explicaremos seus riscos e três alternativas seguras que o ajudarão a relaxar enquanto desfruta de sua funcionalidade.

Quais são as vulnerabilidades do tx.origin?

Acontece que o tx.origin não é uma implementação totalmente segura na maioria dos casos usados pelos desenvolvedores. E pode levar a vulnerabilidades e hacks em seus contratos inteligentes, permitindo que os invasores criem um contrato que chama o contrato destinatário, fazendo com que pareça que o chamador original seja diferente do que realmente é, resultando em manipulações.

E se realmente precisarmos e quisermos a funcionalidade que o tx.origin oferece? Uma solução rápida é usar o msg.sender ao invés do tx.origin , já que ele é menos suscetível a tais invasões. Vamos fazer uma breve revisão sobre o básico das variáveis tx.origin e msg.sender e, em seguida, ver as soluções e os exemplos.

tx.origin vs msg.sender!

O tx.origin e o msg.sender são ambos, variáveis globais nos contratos inteligentes da Ethereum que representam endereços. No entanto, há uma diferença importante entre as duas variáveis.

O tx.origin refere-se ao endereço externo original que iniciou a transação. É o endereço que assinou a transação e a enviou para a rede. Isso é útil em alguns casos, por exemplo, quando você deseja saber quem iniciou uma transação que resultou em uma determinada alteração de estado.

O msg.sender, por outro lado, refere-se ao chamador imediato da função. É o endereço chamado pela função atual, que pode ser um usuário, um contrato ou outra função. Essa variável é comumente usada para implementar o controle de acesso em contratos inteligentes.

Alternativas seguras para as vulnerabilidades do tx.origin

Portanto, as três abordagens seguras a seguir podem ser implementadas para manter a funcionalidade do endereço original que criou uma transação.

1. Usando o msg.sender em uma chamada contrato a contrato:

Nesse método, você pode ter um contrato, chamar outro contrato e passar o valor msg.sender como um argumento. O contrato destinatário pode então verificar se a chamada foi feita a partir de um contrato confiável, verificando se o valor msg.sender é igual ao endereço de contrato confiável. Esse método é seguro porque só permite que contratos confiáveis chamem o contrato destinatário e permite que o contrato destinatário identifique o remetente imediato. Aqui está um exemplo de código que demonstra como usar o msg.sender em uma chamada de contrato para contrato:

// Contrato Confiável

pragma solidity ^0.8.0;

contract TrustedContract {
   address public trustedSender;

   function setSender(address sender) public {
       trustedSender = sender;
   }
}
// Contrato Chamador

pragma solidity ^0.8.0;

contract CallerContract {
   address public trustedContractAddress;
   TrustedContract trustedContract;
constructor(address _trustedContractAddress) {
       trustedContractAddress = _trustedContractAddress;
       trustedContract = TrustedContract(_trustedContractAddress);
   }
function callSetSender() public {
       // Passar msg.sender para o contrato confiável
       trustedContract.setSender(msg.sender);
   }
function checkSender() public view returns(bool) {
       // Verificar se o remetente imediato é o contrato confiável
       return msg.sender == trustedContractAddress;
   }
}
Enter fullscreen mode Exit fullscreen mode

Os dois exemplos de contrato acima TrustedContract e CallerContract mostram que o TrustedContract possui uma variável pública trustedSender que armazena o endereço de uma carteira de remetente confiável. A função setSender desse contrato permite que o remetente defina a variável trustedSender.

O CallerContract é um contrato que vai chamar a função setSender do TrustedContract. O constructor CallerContract recebe um parâmetro de endereço que é o endereço do TrustedContract. Na função callSetSender, o msg.sender é passado para a função setSender do TrustedContract. Isso significa que o remetente imediato do callSetSender ficará gravado como trustedSender no TrustedContract.

A função checkSender do CallerContract verifica se o remetente imediato da chamada de função é o TrustedContract. Isso é feito verificando que msg.sender é igual a trustedContractAddress.

Por fim, você precisa garantir que somente os contratos confiáveis possam chamar a função setSender.

2. Usando uma mensagem assinada:

Nesse método, o remetente assina uma mensagem contendo as informações necessárias e envia a mensagem assinada para o contrato destinatário. O contrato destinatário pode então verificar a assinatura usando a chave pública do remetente e confirmar a identidade do remetente. Esse método é seguro porque se baseia em assinaturas criptográficas, permitindo que o contrato destinatário identifique o remetente.

Vamos dar uma olhada em um código de exemplo que demonstra como usar uma mensagem assinada e verificar a identidade do remetente em um contrato:

// Contrato Remetente

pragma solidity ^0.8.0;

contract SenderContract {
   function signMessage(uint256 amount, address recipient, uint256 nonce) public pure returns (bytes32) {
       return keccak256(abi.encodePacked(amount, recipient, nonce));
   }
}

// Contrato Destinatário

pragma solidity ^0.8.0;

contract ReceiverContract {
   function transfer(uint256 amount, address recipient, uint256 nonce, bytes memory signature) public {
       // Verificar a assinatura usando a chave pública do remetente
       address sender = recoverSigner(amount, recipient, nonce, signature);
       // Realizar a transferência
       // ...
   }


   function recoverSigner(uint256 amount, address recipient, uint256 nonce, bytes memory signature) public pure returns (address) {
       bytes32 messageHash = keccak256(abi.encodePacked(amount, recipient, nonce));
       bytes32 messageHashPrefix = "\x19Ethereum Signed Message:\n32";
       bytes32 prefixedHash = keccak256(abi.encodePacked(messageHashPrefix, messageHash));
       (uint8 v, bytes32 r, bytes32 s) = splitSignature(signature);
       return ecrecover(prefixedHash, v, r, s);
   }

   function splitSignature(bytes memory signature) public pure returns (uint8, bytes32, bytes32) {
       require(signature.length == 65, "Invalid signature length");
       bytes32 r;
       bytes32 s;
       uint8 v;
       assembly {
           r := mload(add(signature, 32))
           s := mload(add(signature, 64))
           v := byte(0, mload(add(signature, 96)))
       }
       return (v, r, s);
   }
}
Enter fullscreen mode Exit fullscreen mode

Nesse código, existem 2 contratos, o SenderContract e o ReceiverContract. O SenderContract possui uma função signMessage que recebe três parâmetros: amount, recipient e nonce. Essa função retorna o hash keccak256 da concatenação desses parâmetros.

O ReceiverContract é o contrato que recebe a mensagem assinada e verifica a identidade do remetente. A função transfer desse contrato recebe quatro parâmetros: amount, recipient, nonce, e signature. Essa função, primeiro verifica a assinatura usando a função recoverSigner. Se a assinatura é válida, a função realiza a transferência.

A função recoverSigner recebe quatro parâmetros: amount, recipient, nonce e signature. Essa função primeiro calcula o hash keccak256 da concatenação de amount, recipient e nonce. Em seguida, ele prefixa o hash com a string "\x19Ethereum Signed Message:\n32" e calcula o hash keccak256 do resultado. Esse hash prefixado é então usado com a função ecrecover para recuperar o endereço assinado.

A função splitSignature é uma função utilitária que extrai os componentes da assinatura v, r e s.

Tenha cuidado para que, no uso no mundo real, apenas remetentes autorizados possam assinar mensagens e enviá-las ao contrato destinatário.

3. Usando um contrato proxy:

No método final, você pode fazer com que um contrato proxy atue como intermediário entre o remetente e o contrato destinatário. O contrato proxy pode verificar a identidade do remetente e, em seguida, chamar o contrato destinatário em nome do remetente, passando as informações necessárias como argumentos. Esse método é seguro porque só permite que contratos ou contas confiáveis chamem o contrato proxy e permite que o contrato destinatário identifique o remetente imediato.

Esta é a abordagem que demonstra o uso de um contrato proxy como intermediário para chamadas seguras de contrato para contrato:

// SenderContract.sol

pragma solidity ^0.8.0;
contract SenderContract {
   address public proxyContract;
   constructor(address _proxyContract) {
       proxyContract = _proxyContract;
   }
   function transfer(address _recipient, uint256 _amount) external {
       // Chamar o contrato proxy com as informações de transferência
       (bool success, ) = proxyContract.call(abi.encodeWithSignature("transfer(address,uint256)", _recipient, _amount));
       require(success, "Transfer failed");
   }
}
// ProxyContract.sol

pragma solidity ^0.8.0;
contract ProxyContract {
   address public trustedAddress;
   constructor(address _trustedAddress) {
       trustedAddress = _trustedAddress;
   }
   function transfer(address _recipient, uint256 _amount) external {
       // Verifique se a chamada é feita a partir do endereço ou contrato confiável
       require(msg.sender == trustedAddress, "Unauthorized");
       // Chamar o contrato destinatário com as informações de transferência
       (bool success, ) = _recipient.call(abi.encodeWithSignature("receiveTransfer(uint256)", _amount));
       require(success, "Transfer failed");
   }
}
// ReceiverContract.sol

pragma solidity ^0.8.0;
contract ReceiverContract {
   address public owner;
   constructor() {
       owner = msg.sender;
   }
   function receiveTransfer(uint256 _amount) external {
       // Realizar a transferência
       // ...
   }
}
Enter fullscreen mode Exit fullscreen mode

Nesse exemplo, o SenderContract chama a função transfer do ProxyContract com as informações de transferência como argumentos. O ProxyContract verifica se a chamada foi feita a partir do endereço ou contrato confiável e, em seguida, chama a função receiveTransfer no ReceiverContract com as informações de transferência como argumentos.

Em uma implantação de produção, certifique-se de que somente contratos ou contas autorizadas possam chamar o contrato proxy.

Concluindo

Embora o tx.origin seja usado para acessar a carteira original que iniciou a transação e, em muitos casos, sendo extremamente útil, seu uso não é uma abordagem favorável na maioria das vezes. Talvez seja necessário acrescentar mecanismos adicionais de controle de acesso ou gerenciamento de permissões para garantir que somente contratos ou contas autorizadas possam chamar sua função e seu contrato, mesmo nas abordagens acima.

Esse artigo foi escrito por D. H. Mood e traduzido por Fátima Lima. O original pode ser lido aqui.

Latest comments (0)