3 Secure Alternative Methods to replace`tx.origin` for solidity developers

D. H. Mood
soonami
Published in
6 min readApr 25, 2023

What is tx.origin?

tx.origin is a global variable in Ethereum smart contracts that represents the original external address that initiated the transaction. That is basically speaking a wallet address we use every day!

It refers to the address that originally created and signed a transaction, regardless of how many contracts the transaction has passed through before reaching its current state, i.e. where the tx.origin variable is being used in the code.

It’s extremely tempting to use tx.origin in the smart contract as it can really simplify the life of us, the developers. But if you’ve spoken to any senior smart contract developer, you’ve defiantly seen them being picky about its use in the code. Why?.

In this article, we will explain its risk and 3 secure alternatives that help you relax while you’re still enjoying its functionality.

What are tx.origin ‘s vulnerabilities?

Turns out tx.origin isn’t totally a safe implementation in most of the developers' used cases. And it can lead to vulnerabilities & hacks in your smart contracts, by enabling attackers who create a contract that calls the target contract, making it appear as if the original caller is different than it actually is, resulting in manipulations.

Now what if we really need and want the functionality tx.origin offered? A quick solution is to use msg.senderinstead, as it is less susceptible to such attacks. Let’s quickly review some basics about the tx.origin & msg.sender variables and then look at the solutions & examples.

tx.origin vs msg.sender !

tx.origin and msg.sender are both global variables in Ethereum smart contracts that represent addresses. However, there is an important difference between the two.

tx.origin refers to the original external address that initiated the transaction. It is the address that signed the transaction and sent it to the network. This is useful in some instances, such as when you want to know who initiated a transaction that resulted in a certain state change.

msg.sender, on the other hand, refers to the immediate caller of the function. It is the address that is called the current function, which may be a user, a contract, or another function. This variable is commonly used to implement access control in smart contracts.

Secure Alternatives to tx.origin ‘s vulnerabilities

So, the following 3 secure approaches can be implemented to retain the functionality of the original address that created a transaction.

1. Using msg.sender in a contract-to-contract call:

In this method, you can have a contract, call another contract and pass the `msg.sender` value as an argument. The receiving contract can then verify that the call was made from a trusted contract by checking that the `msg.sender` value is equal to the trusted contract’s address. This method is secure because it only allows trusted contracts to call the receiving contract, and enables the receiving contract to identify the immediate sender. Here’s a sample code that demonstrates how to use msg.sender in a contract-to-contract call:

// Trusted Contract

pragma solidity ^0.8.0;

contract TrustedContract {
address public trustedSender;

function setSender(address sender) public {
trustedSender = sender;
}
}
// Caller Contract

pragma solidity ^0.8.0;

contract CallerContract {
address public trustedContractAddress;
TrustedContract trustedContract;
constructor(address _trustedContractAddress) {
trustedContractAddress = _trustedContractAddress;
trustedContract = TrustedContract(_trustedContractAddress);
}
function callSetSender() public {
// Pass msg.sender to the trusted contract
trustedContract.setSender(msg.sender);
}
function checkSender() public view returns(bool) {
// Check if the immediate sender is the trusted contract
return msg.sender == trustedContractAddress;
}
}

The above two contracts TrustedContract and CallerContract example shows TrustedContracthas a public variable trustedSender which stores the address of a trusted sender wallet. The setSender function of this contract allows the sender to set the trustedSender variable.

CallerContract is the contract that will call setSender the function of TrustedContract. The constructor CallerContract takes an address parameter which is the address of the TrustedContract. In the callSetSender function, msg.sender is passed to the setSender function of the TrustedContract. This means that the immediate sender of callSetSender will be recorded as trustedSender in the TrustedContract.

The checkSender function of CallerContract verifies that the immediate sender of the function call is the TrustedContract This is done by checking that msg.sender is equal to trustedContractAddress.

Finally, you need to make sure only trusted contracts can call the setSender function.

2. Using a signed message:

In this method, the sender signs a message containing the necessary information and sends the signed message to the receiving contract. The receiving contract can then verify the signature using the sender’s public key and confirm the identity of the sender. This method is secure because it relies on cryptographic signatures, enabling the receiving contract to identify the sender.

Let’s take a look at an example code demonstrating how to use a signed message and verify the sender’s identity in a contract:

// Sender Contract

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));
}
}

// Receiver Contract

pragma solidity ^0.8.0;

contract ReceiverContract {
function transfer(uint256 amount, address recipient, uint256 nonce, bytes memory signature) public {
// Verify the signature using the sender's public key
address sender = recoverSigner(amount, recipient, nonce, signature);
// Perform the transfer
// ...
}

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);
}
}

In this code, there are two contracts: SenderContract and ReceiverContract. SenderContract has a signMessage function that takes three parameters: amount, recipient, and nonce. This function returns the keccak256 hash of the concatenation of these parameters.

ReceiverContract is the contract that receives the signed message and verifies the sender's identity. The transfer function of this contract takes four parameters: amount, recipient, nonce, and signature. This function first verifies the signature using the recoverSigner function. If the signature is valid, the function performs the transfer.

The recoverSigner function takes four parameters: amount, recipient, nonce, and signature. This function first calculates the keccak256 hash of the concatenation of amount, recipient, and nonce. It then prefixes the hash with the string "\x19Ethereum Signed Message:\n32" and calculates the keccak256 hash of the result. This prefixed hash is then used with the ecrecover function to recover the signer's address.

The splitSignature function is a utility function that extracts the v, r, and s components of the signature.

Be careful that in real-world usage ensure that only authorized senders can sign messages and send them to the receiving contract.

3. Using a proxy contract:

In the final method, you can have a proxy contract act as an intermediary between the sender and the receiving contract. The proxy contract can verify the identity of the sender and then call the receiving contract on behalf of the sender, passing the necessary information as arguments. This method is secure because it only allows trusted contracts or accounts to call the proxy contract, and enables the receiving contract to identify the immediate sender.

Here’s the approach that demonstrates the use of a proxy contract as an intermediary for secure contract-to-contract calls:

// SenderContract.sol

pragma solidity ^0.8.0;
contract SenderContract {
address public proxyContract;
constructor(address _proxyContract) {
proxyContract = _proxyContract;
}
function transfer(address _recipient, uint256 _amount) external {
// Call the proxy contract with the transfer information
(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 {
// Verify that the call is made from the trusted address or contract
require(msg.sender == trustedAddress, "Unauthorized");
// Call the receiving contract with the transfer information
(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 {
// Perform the transfer
// ...
}
}

In this example, SenderContract calls ProxyContract’s transfer function with the transfer information as arguments. ProxyContract verifies that the call is made from the trusted address or contract and then calls the receiveTransfer function in ReceiverContract with the transfer information as arguments.

In a production deployment, ensure that only authorized contracts or accounts can call the proxy contract.

Wrapping up

While tx.originis used to access the original wallet that initiated the transaction and in many cases can be extremely useful. Using it is not a favored approach mostly. You may need to add additional access control or permission management mechanisms to ensure that only authorized contracts or accounts can call your function & contract, even in the above approaches.

--

--