Gelato Network
Search…
⌃K
📚

ERC-2771

How you can use this protocol for native meta transactions with top notch security
After reading this page:
  • You'll understand how to use sponsoredCallERC2771 in combination with ERC2771Context to achieve a gasless UX for your app, with secure user signature verification.
  • You'll understand ERC-2771's core functionality and how it allows for the off-chain sender address to be verified on-chain.
When using relayWithSponsoredUserAuthCall , you are sponsoring your user's gas, using 1Balance for payment, and you are asking them to sign off on their transaction's relay request using their private key (for example, via MetaMask). This is for security, and Gelato will verify on-chain the user's signature matches the required address before forwarding the call.
When relaying a message to a target smart contract function, the function needs to authenticate that the message was created by the correct party, and forwarded through the correct relayer. Otherwise, your target function is open to exploits. ERC-2771 uses clever data encoding to allow for a new _msgSender to be relayed from off-chain, and a trustedForwarder address to be set. Together, these two parameters protect against any foul play and allow for information to be sent from off-chain on-chain securely!

Why?

When relaying, the msg.sender loses its informational value. Whereas usually, the msg.sender would be the user initiating the transaction, with off-chain relaying, we lose this valuable information.
For example, how does the target smart contract permission who can call a specific function? In this case, the msg.sender will be the relayer, but whitelisting this address is both permissioned and still not enough to stop some one else using the same relayer from calling your function. This can be especially worrisome when low-level calls are involved.
  • The best option would be somehow to let the relay call originator specify an address and have this address relayed on-chain. The target smart contract can now authenticate a function call using this address.
  • But how do we successfully pass information (a specific address) through low-level calldata from off-chain to on-chain without interfering with the calldata?

ERC-2771's core functionality

Here’s where the magic happens, the trusted forwarder encodes the from address i.e. the off-chain address into the calldata by appending it at the end:
1
(bool success, ) = to.call.value(value)(abi.encodePacked(data, from));
The target contract can now verify the from address by decoding the data in the same way, making sure this message has been passed through the trustedForwarder.
The required target contract function can be sure that the correct entity signed and requested this payload to be relayed, only via a trusted forwarder - in our case, Gelato Relay.

How does Gelato encode this data?

Gelato Relay's sponsoredCallERC2771 function encodes the user's address, which can then be utilised by the ERC-2771 compatible target smart contract. The most relevant part, where the user address is appended to the calldata, is shown below:
GelatoRelayERC2771.soll
1
_call.target.revertingContractCall(
2
_encodeERC2771Context(_call.data, _call.user),
3
"GelatoRelayERC2771.sponsoredCallERC2771:"
4
);
where _encodeERC2771Context refers to:
GelatoRelayUtils.sol
1
function _encodeERC2771Context(bytes calldata _data, address _msgSender)
2
pure
3
returns (bytes memory)
4
{
5
return abi.encodePacked(_data, _msgSender);
6
}
We are encoding the calldata and the user address together by simply appending the user's address to the end as required by ERC-2771.
For the full code, please see here.

How can I modify my smart contract to be ERC-2771 compatible?

0. Install Gelato's relay-context package in your contract repo

See also relay-context-contracts: Installation
npm install --save-dev @gelatonetwork/relay-context
or
yarn add -D @gelatonetwork/relay-context

1. Import the ERC2771Context contract:

1
import {
2
ERC2771Context
3
} from "@gelatonetwork/relay-context/contracts/vendor/ERC2771Context.sol";
This contract's main functionality (originally implemented by OpenZeppelin) is to decode the off-chain "msg.sender" from the encoded calldata using _msgSender() .

ERC2771Context.sol

1
// SPDX-License-Identifier: MIT
2
// OpenZeppelin Contracts (last updated v4.7.0) (metatx/ERC2771Context.sol)
3
4
pragma solidity ^0.8.9;
5
6
import "../utils/Context.sol";
7
8
/**
9
* @dev Context variant with ERC2771 support.
10
*/
11
abstract contract ERC2771Context is Context {
12
address private immutable _trustedForwarder;
13
14
constructor(address trustedForwarder) {
15
_trustedForwarder = trustedForwarder;
16
}
17
18
function isTrustedForwarder(address forwarder) public view virtual returns (bool) {
19
return forwarder == _trustedForwarder;
20
}
21
22
function _msgSender() internal view virtual override returns (address sender) {
23
if (isTrustedForwarder(msg.sender)) {
24
// The assembly code is more direct than the Solidity version using `abi.decode`.
25
/// @solidity memory-safe-assembly
26
assembly {
27
sender := shr(96, calldataload(sub(calldatasize(), 20)))
28
}
29
} else {
30
return super._msgSender();
31
}
32
}
33
34
function _msgData() internal view virtual override returns (bytes calldata) {
35
if (isTrustedForwarder(msg.sender)) {
36
return msg.data[:msg.data.length - 20];
37
} else {
38
return super._msgData();
39
}
40
}
41
}
  • The trustedForwarder variable is set in the constructor which allows for setting a trusted party that will relay your message to your target smart contract. In our case, this is Gelato RelayERC2771.sol @ 0x1Cc587d239AF07C23D8f28Bc6DCdF73BE1994cA1 .
  • The _msgSender() function encapsulates the main functionality of ERC-2771, by decoding the user address from the last 20 bytes of the calldata.
    • In Solidity, the logic is equivalent to:
1
abi.decode(
2
msg.data[msg.data.length - 20:],
3
(address)
4
);
  • Gelato's smart contracts handle the encoding of important information to the calldata (see How does Gelato encode this data?). It is the job of your target smart contract function to decode this information using this _msgSender() function.
  • The _msgData() function decodes the function selector calldata from the entire calldata. This is not used often but can be handy for testing.

2. Replace msg.sender with _msgSender()

Within the function that you would like to be called with Gelato Relay, replace all instances of msg.sender with a call to the _msgSender() function inherited from ERC2771Context. _msgSender() is the off-chain signer of the relay request, allowing for secure whitelisting on your target function.

3. (Re)deploy your contract and whitelist GelatoRelayERC2771

If your contract is not upgradeable, then you will have to redeploy your contract to set GelatoRelayERC2771.sol as your trustedForwarder . GelatoRelayERC2771.sol is immutable for security reasons. This means that once you set GelatoRelayERC2771.sol as your trusted forwarder, there is no way for Gelato to change the ERC2771 signature verification scheme and so after you can verify the code yourself here, you can be sure that the intended _msgSender is correct and accessible from within your target contract.
GelatoRelayERC2771's address is the same on all supported networks:
0xBf175FCC7086b4f9bd59d5EAE8eA67b8f940DE0d