Este artículo es una traducción de D. H. Mood, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_eshttps://twitter.com/web3dev_es en Twitter.
¿Qué es tx.origin
?
tx.origin
es una variable global en los contratos inteligentes de Ethereum que representa las direcciones externas originales que iniciaron la transacción. Básicamente, ¡es hablar de la dirección de la cartera que usamos todos los días!
Se refiere a la dirección que originalmente creó y firmó la transacción, sin importar de cuántos contratos la transacción ha pasado antes de llegar a su estado actual, es decir, donde la variable tx.origin
está siendo usada en el código.
Es extremadamente tentador usar tx.origin
en el contrato inteligente ya que realmente puede simplificar la vida del desarrollador. Pero, si has hablado con cualquier desarrollador senior de contratos inteligentes, has visto que han sido muy quisquillosos acerca de su uso en el código. ¿Por qué?
En este artículo, explicaremos el riesgo y 3 alternativas seguras que te ayudarán a relajarte mientras disfrutas su funcionalidad.
¿Cuáles son las vulnerabilidades de tx.origin
?
Resulta que tx.origin
no es una implementación totalmente segura, en la mayoría de casos de usos de los desarrolladores. Y eso puede llevar a vulnerabilidades y hackeos en tus contratos inteligentes, permitiendo a los atacantes que crean un contrato que invoca el contrato del objetivo, haciendo que parezca como si la persona que originalmente llamó es diferente a quien realmente es, resultando en manipulaciones.
Ahora, ¿qué pasaría si realmente necesitamos y queremos la funcionalidad que tx.origin
ofrece? En cambio, una rápida solución es usar msg.sender
, ya que es menos susceptible a dichos ataques. Veamos rápidamente algunos aspectos básicos sobre las variables tx.origin
y msg.sender
y luego ver las soluciones y ejemplos.
tx.origin
vs msg.sender
tx.origin
y msg.sender
son variables globales en los contratos inteligentes de Ethereum que representan la dirección. Aún así, hay una importante diferencia entre las dos.
tx.origin
se refiere a la dirección externa original que inició la transacción. Es la dirección que firmó la transacción y la envió a la red. En algunas instancias, esto es útil como cuando necesitas saber quién inició una transacción que resultó en cierto cambio de estado.
En contraparte, msg.sender
se refiere a la función de llamada inmediata. Es la dirección que es llamada en la función actual, la cual puede ser un usuario, un contrato u otra función. Esta variable es comúnmente usada para implementar acceso de control en contratos inteligentes.
Alternativas seguras a las vulnerabilidades de tx.origins
Así que, las siguientes tres alternativas seguras pueden ser implementadas para retener la funcionalidad de la dirección original que creó la transacción.
1 Usar msg.sender
en una llamada contrato a contrato
En este método, pueden tener un contrato, invocar otro contrato y pasar el valor msg.sender
como argumento. El contrato que recibe puede entonces verificar que la llamada fue hecha desde un contacto de confianza, revisando que el valor de msg.sender
es igual a la dirección del contrato del que se confía. Este método es seguro porque solo permite contratos de confianza para invocar el contrato que se recibe y permite que el contrato que se recibe identifique al remitente inmediato.
Aquí hay un código simple que demuestra cómo usar msg.sender
en una llamada de contrato a contrato:
// Contrato confiado
pragma solidity ^0.8.0;
contract TrustedContract {
address public trustedSender;
function setSender(address sender) public {
trustedSender = sender;
}
}
// Contrato Iivocado
pragma solidity ^0.8.0;
contract CallerContract {
address public trustedContractAddress;
TrustedContract trustedContract;
constructor(address _trustedContractAddress) {
trustedContractAddress = _trustedContractAddress;
trustedContract = TrustedContract(_trustedContractAddress);
}
function callSetSender() public {
// Pasa msg.sender al contrato confiado
trustedContract.setSender(msg.sender);
}
function checkSender() public view returns(bool) {
// Revisa si el remitente inmediato es el contrato confiado
return msg.sender == trustedContractAddress;
}
}
Los dos ejemplos de contratos de arriba, TrustedContract
y CallerContract
, muestran que TrustedContract
tiene una variable pública y trustedSender
almacena la dirección de la dirección del remitente de confianza. La función setSender, de este contrato, permite al remitente configurar la variable trustedSender.
CallerContract es el contrato que llamará la función setSender
de TrustedContract. El constructor CallerContract
toma la dirección del parámetro, el cual es la dirección de TrustedContract. En la función callSetSender, msg.sender
es pasado de la función setSender
de TrustedContract
. Esto significa que el remitente inmediato de callSetSender será registrado como trustedSender
en TrustedContract
.
La función checkSender
de CallerContract
, verifica que el remitente inmediato de la función de llamada es TrustedContract
. Esto se hace revisando que msg.sender
sea igual a trustedContractAddress
.
Finalmente, necesitas asegurarte que solo los contratos de confianza puedan invocar a la función set\sender
.
2: usando el mensaje firmado
En este método, el remitente firma un mensaje que contiene la información necesaria y envía el mensaje firmado al contrato receptor. El contrato receptor puede verificar la firma usando la clave pública del remitente y confirma la identidad del remitente. Este método es seguro porque depende de las firmas criptográficas, permitiendo que el contrato receptor pueda identificar el remitente.
Veamos un ejemplo de un código demostrando cómo usar un mensaje firmado y verificar la identidad del remitente en un contrato:
// Contrato del Remitente
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 del Receptor
pragma solidity ^0.8.0;
contract ReceiverContract {
function transfer(uint256 amount, address recipient, uint256 nonce, bytes memory signature) public {
// Verifica que la firma usa la clave pública del remitente
address sender = recoverSigner(amount, recipient, nonce, signature);
// Realiza la transferencia
// ...
}
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);
}
}
En este código, hay dos contratos: SenderContract
y ReceiverContract
. SenderContract
tiene una función signMessage
que toma los tres parámetros: amount
, recipient
y nonce
. Esta función retorna el hash keccak256 de la concatenación de estos parámetros.
ReceiverContract
es el contrato que recibe el mensaje firmado y verifica la identidad del remitente. La función transfer
de este contrato toma cuatros parámetros: amount
, recipient
, nonce
y signature
. Esta función verifica primero la firma, utilizando la función recoverSigner
. Si la firma es válida, la función realizará la transferencia.
La función recoverSigner
toma cuatros parámetros: amount
, recipient
, nonce
y signature
. Esta función calcula primero el hash keccak256 de la concatenación de amount
, recipient
y nonce
. Luego prefija el hash con la cadena “\x19Ethereum Signed Message:\n32” y calcula el hash keccak256 del resultado. Este hash prefijado es luego usado con la función ecrecover
para recuperar la dirección del firmante.
La función splitSignature
es una función de utilidad que extrae los componentes v
, r
y s
de la firma.
Ten cuidado ya que el uso en la vida real asegura que sólo los remitentes autorizados puedan firmar mensajes y enviarlos a los contratos receptores.
Usando un contrato proxy:
En el método final, puedes tener un contrato proxy para que actúe como un intermediario entre el remitente y el contrato receptor. El contrato proxy puede entonces verificar la identidad del remitente y luego invocar el contrato receptor a nombre del remitente, pasando la información necesaria como argumentos. Este método es seguro porque solo permite a contratos de confianza o cuentas, llamar al contrato proxy y permite que el contrato receptor sea identificado con el remitente inmediato.
Aquí hay un acercamiento que demuestra el uso del contrato proxy como un intermediario para llamadas seguras de contrato a 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 {
// Invoca el contrato proxy con la información de transferencia
(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 {
// Verifica que la llamada es hecha desde la dirección de confianza o cuenta
require(msg.sender == trustedAddress, "Unauthorized");
// Invoca el contrato receptor con la información de la transferencia
(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 {
// Realiza la transferencia
// ...
}
}
En este ejemplo, SenderContract invoca a la función transfer
de ProxyContract con la información de la transferencia como argumentos. ProxyContracts verifica que la llamada sea hecha desde la dirección de confianza o el contrato y luego invoca la función receiveTransfer
en ReceiverContract con la información de transferencia como argumentos.
En el despliegue de producción, asegúrate que sólo las cuentas o contratos autorizados puedan llamar al contrato proxy.
Terminando
Mientras que tx.origin
es usada para acceder a la cartera original que inició la transacción y, en muchos casos, puede ser extremadamente útil. Usarla no es un acercamiento muy favorable, la mayoría del tiempo. Puede que necesites añadir accesos de control adicionales o mecanismos de administración de permisos para asegurarte que sólo las cuentas o contratos autorizados puedan llamar tu función y contrato, incluso en los acercamientos de arriba.
Discussion (0)