3 Secure Alternative Methods to replace`tx.origin` for solidity developers
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.sender
instead, 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 TrustedContract
has 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.origin
is 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.