Contract calls contract - forwarding msg.sender - could tx.origin be the solution?

I am developing smart contracts for a while so I thought this should be doable, but somehow I can't find a solution.

I have a (staking) contract c1 and a (new staking) contract c2.
I want to provide the opportunity for users to migrate staked funds from c1 to c2 with one transaction (from the user's perspective).
User would either call a dedicated migration contract or a migrate function in c2.
Old c1 contract (which is already deployed and immutable) has a withdrawAll() function (with no parameters) which returns all funds back to a user (=msg.sender).

  1. Ideally user would call migrate() in contract c2
  2. c2 would (on behalf of the user) call withdrawAll() in c1
  3. tokens would be send to users wallet
  4. tokens would be staked in new contract c2

The problem is, that when c2 calls withdrawAll() function in c1, the msg.sender will be c2 , not the user's account any more and c1 would refuse the transaction to unstake the funds.

I looked into delegateCall, yes , msg.sender would be set to the user's account when c2 calls withdrawAll() function in c1 , but state variables in c1 could not be updated (which keep track of staked amount of each user).

Any suggestions welcome ...

In sum, you are asking if c1 can impersonate msg.sender when interacting with c2. Not possible, it would be a huge security hole (see tx.origin and the security issues around its usage).

Yes, I understand. I was just hoping that there is a way as it would happen in the context of the original transaction initiated by the user.
(I will re-visit the tx.origin topic as well.)

contract c2 {
    function migrate() public {
        c1.withdrawAll();
        stake(balance);
    }
}
contract c1 {
    mapping(address => uint256) public stakedAmount;
    function withdrawAll() public {
        uint256 amount = stakedAmount[msg.sender];
        stakedAmount[msg.sender] = 0;
        IERC20(tokenAddress).safeTransfer(msg.sender, amount);
    }
}

Looks like tx.origin would actually do what I was looking for.
User calls function in c2, which in turn calls function in c1, and the account of the user who initiated the call would be available.

However, everywhere the usage of tx.origin is discouraged as it may be a security issue.
Like here https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/tx-origin/

Altough it would not help in my case, as I can't update the c1 contract, it looks like as if tx.origin could be used (safely ?).

  • c2 and c1 have the address of the other contract stored, so should be able just to limit access from the other one.

  • When transferring token and not sending ETH, the attack scenario which is normally mentioned in the context of tx.origin does not apply. In my use case, I am also in control of the token contract which would be called on transfer.

Is there anything which would still put the funds at risk in context of this usage of tx.origin.
Aren't there useful and safe usage scenarios or is the usage of tx.origin in any case dangerous ?

Any thoughts ?

It doesn't matter. It a authorization checking problem. tx.origin is set to the original EOA account that sent the transaction, while msg.sender is set to the caller contract/EOA.

Say account A interacts with contract X, which call a method on Y, which calls a method on Z. all contracts will have tx.originset to A. this means that a malicious actor can write a contract that interacts with X and convince a user to interact with that malicious contract.

It's also not guaranteed that tx.origin will keep working in the future (according to vitalik).

If all your withdraw functions are checking msg.sender, there isn't much to do.

I'm assuming that your contract doesn't mint tokens and instead just keeps users balances

1 Like

Thanks for the additional explanantion.
Yes, right. Contract Z would not be able to see if there is a man-in-the-middle-attack.

The only idea I can think of atm would be that all contracts would need some additional access control logic. In my example ...

  • c1 holds staked user's (A) funds
  • user can directly call c1 to withdraw funds
  • However, user can also call c2, which would in turn calls c1, (A -> c2 -> c1) , but c1 one would need to know c2's address so it can restrict calls to it's function to c2 (and user) only.
  • c2 would also need to forward original msg.sender (= user account) to c1

Does not look ideal to me, but should be doable, however not in my case as I can not change the code of c1 any more (which is just checking msg.sender).

Really surprised that there is no nicer solution and also that I never stumbled across the requirement that a contract would just need to call two other functions in other contract(s) on behalf of the user.
I thought "worst case" it would be similar to a proxy, but what I have seen in OZ proxies, only the logic of the called contract would be used (with delegateCall), but the called contract state can not be changed.

Probably a nicer way to represent user funds is by creating a token and minting on deposit and burning on withdraw. You can then use the ERC20 functionality (transfer, approve, transferFrom)

that is the problem. if possible, what prevents you from calling the same functions on my behalf? or someone else?

1 Like

Interesting idea, "just" doing it the "liquid staking" way ... I keep that in mind for my next iteration.