Firm Relayer

Overview

Firm Relayer is a ERC-2771 meta-transactions relayer. Contracts that trust the relayer will accept calls as if they came from other accounts. The relayer authenticates requests to relay a batch of calls that are executed atomically by checking an EIP-712 signature by the originating account.

Given its critical nature when it is trusted by other contracts, Firm Relayer is not upgradeable. Contracts, in Firm’s case, modules, are able to add or remove relayers at any moment.

On Firm protocol, via Firm Factory, all modules which are initialized start trusting an instance of Firm Relayer.

Note: Firm Relayer will be discontinued as account abstraction EIPs (namely ERC-4337) are adopted. It was built to allow gasless/sponsored transactions and secure batched actions which are better fit at the wallet level.

Note: Firm Relayer might be discontinued as account abstraction EIPs (namely ERC-4337) are adopted. It was built to allow gasless/sponsored transactions and secure batched actions which are better fit at the wallet level.

Assertions

Even though the relay logic is pretty standard and indeed was heavily inspired in OpenZeppelin’s implementation of it, Firm Relayer introduces the concept of assertions which allow checking the return value of calls.

This allows safely chaining/batching actions which need to ensure that a given call returned a value which has an impact in the calldata of subsequent calls. As all calls in a batch are precomputed, assertions allow injecting a sanity check to stop execution should a transaction been mined before the batch was executed.

An example in Firm’s context: A relay request contains a batch of calls to Roles. The first call creates a new role (Roles:createRole(bytes32 roleAdmins, string memory name)(uint8 roleId)) and then there are several other calls to assign the role to some accounts (Roles:setRole(address user, uint8 roleId, bool isGrant)).

Since roleIds are assigned incrementally by creation order, whoever creates the relay request needs to calculate which will be the id of the next role to be created (by simulating the transaction or checking the number of existing roles) so it can be passed as a parameter to the setRole actions. However if another role is created between the moment in which the relay payload is created and signed and when it’s executed, the role that the batch creates will get a different roleId, and the batch will assign a different role to those accounts.

An assertion in this context would allow to ensure that the roleId returned from the createRole call is the one expected (which is passed as input to the setRole calls) or revert.

Entrypoints

Relay

function relay(RelayRequest calldata request, bytes calldata signature) external payable

Relay is the entrypoint for performing a metatransaction in which the sender of the transaction is decoupled from the actor who is executing the call.

Prior to executing the batch of actions in the relay request, Firm Relayer verifies the following:

  • Relay nonce: in order to prevent replays (actor signs once but the payload is executed several times), Firm Relayer keeps an account nonce counter. Each request needs to specify the current account nonce which starts at zero. If the nonce is incorrect, the relay will revert.

  • Signature: an EIP-712 signature of the RelayRequest struct. It must be exactly 65 bytes with the following format [r (32 bytes)][s (32 bytes)][v (1 byte)]. request.from must be the account that gets recovered from the signature.

If the verification succeeds, FirmRelayer will execute all the calls in the request one by one, setting the correct ERC-2771 context for the call (sender = request.from).

If any call reverts, the relay action will revert, effectively reverting all previous calls. If a call is successful and it has an assertion associated with it, it will read the return data of the call and check that the return value is the one expected by the assertion. If this check fails, the entire execution will be reverted as well.

Self-relay

function selfRelay(Call[] calldata calls, Assertion[] calldata assertions) external payable

There’s an additional entrypoint which allows using Firm Relayer for batching only. Using it won’t perform the relay checks, but the ERC-2771 sender gets set the the msg.sender of the call to Firm Relayer.

It was built to allow using the assertions feature for batched actions even if metatransactions aren’t used.

Last updated