Comment on page
2⃣
callWithSyncFeeERC2771
Transactions with on-chain payments and ERC2771 authentication support
If you are using
@gelatonetwork/relay-sdk
v3 or contracts from the package @gelatonetwork/relay-context
v2 please follow this migration guide to migrate to the new versions.After reading this page:
- You'll see some code which will help you send a relay request within minutes.
- You'll learn how to pay for transactions using the provided values for
fee
,feeToken
andfeeCollector
.
Please proceed to our Security Considerations page and read it thoroughly before advancing with your implementation. It is crucial to understand all potential security risks and measures to mitigate them.
When using
callWithSyncFeeERC2771
relay method the target contract assumes responsibility for transferring the fee to Gelato's fee collector during transaction execution. For this, the target contract needs to know:fee
: the transfer amountfeeToken
: the token to be transferredfeeCollector
: the destination address for the fee
- 1.By inheriting the GelatoRelayContextERC2771 contract in your target contract, you have the ability to transfer the fee through one of two straightforward methods:
_transferRelayFee()
or_transferRelayFeeCapped(uint256 maxFee)
. In either case, the inherited contract takes care of decoding thefee
,feeToken
, andfeeCollector
behind the scenes. The Gelato Relay backend simplifies the process by automatically calculating the fee for you, using Gelato's Fee Oracle to perform the calculations in the background. - 2.Alternatively, you may choose to inherit the GelatoRelayFeeCollectorERC2771 contract. With this approach, Gelato decodes only the
feeCollector
. You must provide thefee
andfeeToken
on-chain, either by hardcoding them (which is not recommended) or embedding them within the payload to be executed. The suggested way to handle this is to calculate the fee with Gelato's Fee Oracle.
This modular design ensures a smooth integration with Gelato's fee handling mechanisms, providing a flexible and user-friendly approach to managing transaction fees within your dApps.
Setting a maximum fee, or
maxFee
, for your transactions is strongly advised. This practice enables you to ensure that transaction costs remain below a specific limit. The method _transferRelayFeeCapped(uint256 maxFee)
in the GelatoRelayContextERC2771 contract provides a convenient way to set the maxFee
easily.If you are utilizing the GelatoRelayFeeCollectorERC2771 contract, the recommended way to pass the
maxFee
is by calculating the fee with Gelato's Fee Oracle, which is accessible in the Relay SDK. The getEstimatedFee()
method is provided to facilitate this calculation.This method initiates the signing of ERC2771 requests with the provided
BrowserProvider
or Wallet
. Once the signature is obtained, the request is forwarded to Gelato.const callWithSyncFeeERC2771 = async (
request: CallWithSyncFeeERC2771Request | CallWithSyncFeeConcurrentERC2771Request,
signerOrProvider: ethers.BrowserProvider | ethers.Signer,
options?: RelayRequestOptions,
apiKey?: string
): Promise<RelayResponse>
signerOrProvider
: a valid provider connected to RPC or a signer.apiKey
: an optional API key that links your request to your Gelato Relay account. As this call pertains to the syncFee payment method, transaction costs won't be deducted from your 1Balance account. By using the API key, you can benefit from increased rate limits of your Gelato Relay account.
type RelayResponse = {
taskId: string;
};
This method starts the signing process for ERC2771 requests using the given
BrowserProvider
or Signer
. After capturing the signature, it returns both the signature and the message. This collected data can then be used with the callWithSyncFeeERC2771WithSignature
method to send the request to Gelato.getSignatureDataERC2771 = (
request: CallWithERC2771Request | CallWithConcurrentERC2771Request,
signerOrProvider: ethers.BrowserProvider | ethers.Signer,
type: ERC2771Type
): Promise<SignatureData>
signerOrProvider
: a valid provider connected to RPC or a signer.
type SignatureData = ConcurrentSignatureData
| SequentialSignatureData;
type ConcurrentSignatureData = {
struct: CallWithConcurrentERC2771Struct;
signature: string;
};
type SequentialSignatureData = {
struct: CallWithERC2771Struct;
signature: string;
};
struct
: EIP-712 message data.signature
: EIP-712 signature.
This method provides the message data intended for external signing along with the EIP-712 typed data. After obtaining the signature, the request can be dispatched using the
callWithSyncFeeERC2771WithSignature
method.getDataToSignERC2771 = (
request: CallWithERC2771Request | CallWithConcurrentERC2771Request,
type: ERC2771Type,
signerOrProvider?: ethers.BrowserProvider | ethers.Signer,
): Promise<PayloadToSign>
signerOrProvider
(optional): A provider needed in a sequential flow to obtain the nonce from the smart contract. If you're providing the nonce within your request or if you're using the concurrent flow, this parameter isn't necessary.
type PayloadToSign = ConcurrentPayloadToSign | SequentialPayloadToSign;
type ConcurrentPayloadToSign = {
struct: CallWithConcurrentERC2771Struct;
typedData: CallWithSyncFeeConcurrentERC2771PayloadToSign;
};
type SequentialPayloadToSign = {
struct: CallWithERC2771Struct;
typedData: CallWithSyncFeeERC2771PayloadToSign;
};
struct
: EIP-712 message data.typedData
: EIP-712 typed data.
This method sends pre-signed requests to Gelato.
const callWithSyncFeeERC2771WithSignature = async (
struct: CallWithERC2771Struct | CallWithConcurrentERC2771Struct;
syncFeeParams: BaseCallWithSyncFeeParams;
signature: string;
options?: RelayRequestOptions;
apiKey?: string
): Promise<RelayResponse>
struct
: EIP-712 message data returned from the signing methods.syncFeeParams:
thefeetoken
andisRelayContext
params.signature:
EIP-712 signature returned after signing the request.apiKey
: an optional API key that links your request to your Gelato Relay account. As this call pertains to the syncFee payment method, transaction costs won't be deducted from your 1Balance account. By using the API key, you can benefit from increased rate limits of your Gelato Relay account.
type RelayResponse = {
taskId: string;
};
As of today, we support two distinct ways of sending
callWithSyncFeeERC2771
requests:- 1.Sequentially: This approach ensures that each request is ordered and validated against the
nonce
stored on-chain. You have two options in this method:- Fetch the current
nonce
value from the smart contract yourself and include it with your request. - Allow the relay-sdk to fetch the
nonce
value for you when handling your relay request.
- 2.Concurrently: This method enables you to send multiple transactions simultaneously. Replay protection is achieved using a hash-based
salt
mechanism. Again, you have two options:- Provide your own
salt
value. - Allow the relay-sdk to generate a unique
salt
value for you when processing your relay request.
By default
callWithSyncFeeERC2771
requests are using the sequential method.Concurrent ERC2771 support has been introduced in the relay-sdk version
5.1.0
. Please make sure that your package is up-to-date to start using it.type SequentialERC2771Request = {
chainId: BigNumberish;
target: string;
data: BytesLike;
user: string;
userDeadline?: BigNumberish;
feeToken: string;
isRelayContext?: boolean;
isConcurrent?: false;
userNonce?: BigNumberish;
};
type ConcurrentERC2771Request = {
chainId: BigNumberish;
target: string;
data: BytesLike;
user: string;
userDeadline?: BigNumberish;
feeToken: string;
isRelayContext?: boolean;
isConcurrent: true;
userSalt?: string;
};
chainId
: the chain ID of the chain where thetarget
smart contract is deployed.target
: the address of the target smart contract.data
: encoded payload data (usually a function selector plus the required arguments) used to call the requiredtarget
address.user
: the address of the user's EOA.userDeadline
: optional, the amount of time in seconds that a user is willing for the relay call to be active in the relay backend before it is dismissed.- This way the user knows that if the transaction is not sent within a certain timeframe, it will expire. Without this, an adversary could pick up the transaction in the mempool and send it later. This could transfer money, or change state at a point in time which would be highly undesirable to the user.
feeToken
: the address of the token that is to be used for payment. Please visit SyncFee Payment Tokens for the full list of supported payment tokens per network.isRelayContext
: an optional boolean (default:true
) denoting what data you would prefer appended to the end of the calldata.- If set to
true
(default), Gelato Relay will append thefeeCollector
address, thefeeToken
address, and the uint256fee
to the calldata. In this case your target contract should inherit from the GelatoRelayContextERC2771 contract. - If set to
false
, Gelato Relay will only append thefeeCollector
address to the calldata. In this case your target contract should inherit from theGelatoRelayFeeCollectorERC2771
contract.
isConcurrent
: false (default), optional, represents that the users' requests are validated based on a nonce, which enforces them to be processed sequentially.userNonce
: optional, this nonce, akin to Ethereum nonces, is stored in a local mapping on the relay contracts. It serves to enforce the nonce ordering of relay calls if the user requires sequential processing. If this parameter is omitted, the relay-sdk will automatically query the current value on-chain.
isConcurrent
: true, indicates that the users' requests are validated based on a unique salt, allowing them to be processed concurrently. Replay protection is still ensured by permitting each salt value to be used only once.userSalt
: optional, this is a bytes32 hash that is used for replay protection. If the salt is not provided then relay-sdk would generate a unique value based on a random seed and a timestamp.
1
// SPDX-License-Identifier: MIT
2
pragma solidity 0.8.17;
3
4
import {
5
GelatoRelayContextERC2771
6
} from "@gelatonetwork/relay-context/contracts/GelatoRelayContextERC2771.sol";
7
8
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
9
10
// Inheriting GelatoRelayContext gives access to:
11
// 1. _getFeeCollector(): returns the address of Gelato's feeCollector
12
// 2. _getFeeToken(): returns the address of the fee token
13
// 3. _getFee(): returns the fee to pay
14
// 4. _transferRelayFee(): transfers the required fee to Gelato's feeCollector.abi
15
// 5. _transferRelayFeeCapped(uint256 maxFee): transfers the fee to Gelato
16
// only if fee < maxFee
17
// 6. function _getMsgSender(): decodes and returns the user's address from the
18
// calldata, which can be used to refer to user safely instead of msg.sender
19
// (which is Gelato Relay in this case).
20
// 7. _getMsgData(): returns the original msg.data without appended information
21
// 8. onlyGelatoRelay modifier: allows only Gelato Relay's smart contract
22
// to call the function
23
contract CounterRelayContextERC2771 is GelatoRelayContextERC2771 {
24
using Address for address payable;
25
26
mapping(address => uint256) public contextCounter;
27
28
// emitting an event for testing purposes
29
event IncrementCounter(address msgSender);
30
31
// `increment` is the target function to call.
32
// This function increments a counter variable which is
33
// mapped to every _getMsgSender(), the address of the user.
34
// This way each user off-chain has their own counter
35
// variable on-chain.
36
function increment() external onlyGelatoRelayERC2771 {
37
// Payment to Gelato
38
// NOTE: be very careful here!
39
// if you do not use the onlyGelatoRelay modifier,
40
// anyone could encode themselves as the fee collector
41
// in the low-level data and drain tokens from this contract.
42
_transferRelayFee();
43
44
// Incrementing the counter mapped to the _getMsgSender()
45
contextCounter[_getMsgSender()]++;
46
47
emit IncrementCounter(_getMsgSender());
48
}
49
50
// `incrementFeeCapped` is the target function to call.
51
// This function uses `_transferRelayFeeCapped` method to ensure
52
// better control of gas fees. If gas fees are above the maxFee value
53
// the transaction will not be executed.
54
// The maxFee will be passed as an argument to the contract call.
55
// This function increments a counter variable by 1
56
// IMPORTANT: with `callWithSyncFee` you need to implement
57
// your own smart contract security measures, as this
58
// function can be called by any third party and not only by
59
// Gelato Relay. If not done properly, funds kept in this
60
// smart contract can be stolen.
61
function incrementFeeCapped(uint256 maxFee) external onlyGelatoRelayERC2771 {
62
63
// Payment to Gelato
64
// NOTE: be very careful here!
65
// if you do not use the onlyGelatoRelay modifier,
66
// anyone could encode themselves as the fee collector
67
// in the low-level data and drain tokens from this contract.
68
69
_transferRelayFeeCapped(maxFee);
70
71
// Incrementing the counter mapped to the _getMsgSender()
72
contextCounter[_getMsgSender()]++;
73
74
emit IncrementCounter(counter);
75
}
76
}
77
1
import { GelatoRelay, CallWithSyncFeeERC2771Request } from "@gelatonetwork/relay-sdk";
2
const relay = new GelatoRelay();
1
// target contract address
2
const counter = "<your counter contract address>";
3
4
// using a human-readable ABI for generating the payload
5
const abi = ["function increment()"];
6
7
// address of the token used to pay fees
8
const feeToken = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
9
10
// connect to the blockchain via a front-end provider
11
const provider = new ethers.BrowserProvider(window.ethereum);
12
const signer = provider.getSigner();
13
const user = signer.getAddress();
14
15
// instantiate the target contract object
16
const contract = new ethers.Contract(counter, abi, signer);
17
18
// example calling the increment() method
19
const { data } = await contract.increment.populateTransaction();
20
21
// populate the relay SDK request body
22
const request: CallWithSyncFeeERC2771Request = {
23
chainId: (await provider.getNetwork()).chainId,
24
target: counter,
25
data: data,
26
user: user,
27
feeToken: feeToken,
28
isRelayContext: true,
29
};
30
31
// send relayRequest to Gelato Relay API
32
const relayResponse = await relay.callWithSyncFeeERC2771(request, provider);
33
34
// -----------------------------------------------------------------
35
// the following is an alternative example using Gelato Fee Oracle,
36
// setting maxFee, and calling the incrementFeeCapped(maxFee) method
37
38
// retrieve the estimate fee from Gelato Fee Oracle
39
const fee = await relay.getEstimatedFee(
40
(await provider.getNetwork()).chainId,
41
feeToken,
42
gasLimit,
43
false,
44
)
45
46
// you can use 2x or 3x to set your maxFee
47
const maxFee = fee * 2
48
49
// example calling the incrementFeeCapped(maxFee) method
50
const { dataMaxFee } = await contract.incrementFeeCapped.populateTransaction(maxFee);
51
52
// populate the relay SDK request body
53
const requestMaxFee: CallWithSyncFeeERC2771Request = {
54
chainId: (await provider.getNetwork()).chainId,
55
target: counter,
56
data: dataMaxFee,
57
user: user,
58
feeToken: feeToken,
59
isRelayContext: true,
60
};
61
62
// send relayRequest to Gelato Relay API
63
const relayResponseMAxFee = await relay.callWithSyncFeeERC2771(requestMaxFee, provider);
Last modified 1mo ago