WEB3DEV Español

Cover image for 3 Métodos Alternativos de Seguridad para reemplazar `tx.origin` para los desarrolladores de Solidity
Hector
Hector

Posted on

3 Métodos Alternativos de Seguridad para reemplazar `tx.origin` para los desarrolladores de Solidity

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.

Image description

¿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;
   }
}
Enter fullscreen mode Exit fullscreen mode
// 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;
   }
}
Enter fullscreen mode Exit fullscreen mode

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));
   }
}
Enter fullscreen mode Exit fullscreen mode
// 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);
   }
}
Enter fullscreen mode Exit fullscreen mode

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");
   }
}
Enter fullscreen mode Exit fullscreen mode
// 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");
   }
}
Enter fullscreen mode Exit fullscreen mode
// ReceiverContract.sol

pragma solidity ^0.8.0;
contract ReceiverContract {
   address public owner;
   constructor() {
       owner = msg.sender;
   }
   function receiveTransfer(uint256 _amount) external {
       // Realiza la transferencia
       // ...
   }
}
Enter fullscreen mode Exit fullscreen mode

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)