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;
}
}
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);
}
}
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
// ...
}
}
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.
Oldest comments (0)