What will msg.sender() in ERC2771 context returns


    function _msgSender() internal view virtual override returns (address sender) {
        if (isTrustedForwarder(msg.sender) && msg.data.length >= 20) {
            // The assembly code is more direct than the Solidity version using `abi.decode`.
            /// @solidity memory-safe-assembly
            assembly {
                sender := shr(96, calldataload(sub(calldatasize(), 20)))
            }
        } else {
            return super._msgSender();
        }
    }

i am a little confused about this method as this method stores the original message sender address by taking last 20 bytes of the data we have sent, and what i know for now is that this data is not signed which we send in the method through the transaction.

for now, i have a method of mint that takes an address as a parameter. when I encode this function and use this data, it works and returns the parameter value i sent in this case which the is original signer address.

 var txData = contract.methods.mint(address).encodeABI();

var transaction = await web3.eth.accounts.signTransaction(
    {
      from: RELAYER_ADDRESS,
      to: req.To, // contract address
      data: txData,
      gas: 300000,
    },
    RELAYER_PRIVATE_KEY
  );

  await web3.eth
    .sendSignedTransaction(transaction.rawTransaction)
    .on("receipt", (receipt) => {
      console.log(receipt);
    });

in case i have no parameters and when i encode it and use this. in this case, maybe the data size is less than 20 so it returned the address of the sender who paid for the gas fee.

 var txData = contract.methods.mint().encodeABI();

var transaction = await web3.eth.accounts.signTransaction(
    {
      from: RELAYER_ADDRESS,
      to: req.To, // contract address
      data: txData,
      gas: 300000,
    },
    RELAYER_PRIVATE_KEY
  );

  await web3.eth
    .sendSignedTransaction(transaction.rawTransaction)
    .on("receipt", (receipt) => {
      console.log(receipt);
    });

But i have seen some examples where they are taking parameters other than address and it is still working for them.

The last 20 bytes are only used as the address when the call comes from the trusted forwarder. In your code you seem to be sending the function call directly to the contract.

so in case if i forward the request through the forwarder contract how it get the address of user who want the gasless transaction from data which is a contract method encoded with parameters?

The question is a bit "overloaded" with details, but it is nevertheless an interesting one, so I'll try to make it more focused:

It is understandable that the relayer can execute a function which takes an address input parameter, because such function can internally determine the original msg.sender (who doesn't pay the transaction gas cost).

But how can the relayer execute in the same manner a function which doesn't take any input, in which case, msg.data.length < 20 (equal to the length of the function selector, i.e., only 4)?


UPDATE:

It probably needs some more rephrasing.

The point of the matter is, that an additional value - the original msg.sender - which is NOT part of the function input parameters, is encoded into msg.data, so the condition msg.data.length >= 20 is guaranteed to hold even for a function which takes no input parameters.

The question is, how is it possible to just "push" another value into msg.data?
Isn't it supposed to reflect only the function selector plus the values of the function input parameters?

yes this is what happening for now

Not exactly, You need at least the function selector (4 bytes) and the encoded parameters, but you can add anything extra after that. The extra part will be considered by the decoder and can be used by "custom" workflows such as ERC-2771.

Right, so it IS possible, and the only remaining question is technical:

one thing I am getting unsure if the account that is paying for the gas fee also has to sign the transaction. like in the case above only the account that is signing the transaction is the person paying for the gas fee.

I'm not sure I understand your question but this is the line of code where the signer is concatenated at the end of the call data, before making the call to the target contract.

Note that we are pushing to msg.data directly. We are concatenating it in memory and then using it to make a call to another contract, this is when it becomes msg.data.

2 Likes