Announcing SignPass: Permissioned Gas Sponsorship with Meta Transaction and Proxy

Jack Boyuan Xu
Sign
Published in
5 min readOct 4, 2021

In collaboration with ARCx, we have released 10 EthSign SignPass NFT skins that complement their DeFi Passport earlier this week. As a perk, we plan to sponsor all gas fees generated by SignPass NFT holders while using EthSign. Technically, this involves utilizing permission control and meta transactions to only sponsor gas if the caller is the owner of our NFT. These topics are trivial to implement on their own but things can get a bit complicated when we try to implement them together. In addition, we want whatever we end up making to be modular, generic, and most importantly plug-n-play. This proved to be quite a fun challenge, so let’s dive right into the technical details!

Code and annotations: https://github.com/boyuanx/Solidity-Calls

The Problem

The simplest thing to do is to build ERC2771 compliance right into EthSign 3.0’s smart contract. Although it was deployed months ago, it is upgradeable and thus we have the power to alter its implementation. However, we would like to avoid this for multiple reasons:

  • If the code is working well, then try to not modify it.
  • It introduces tight coupling between components.
  • We want the solution to be generic and modular.

The Plan

“All problems in computer science can be solved by another level of indirection.” — David Wheeler

The only way to make our solution truly plug-n-play without affecting the existing contract is to use a proxy. Proxies are nothing new — it is the underlying mechanism that enables upgradeable smart contracts. In essence, the proxy keeps track of the address of the underlying implementation contract and delegates the execution to the implementation while managing storage by itself. The use of delegatecall enables contract logic to be updated without affecting storage.

In our use case, we would like to build a proxy that only acts as a permissioned ERC2771 gateway that checks the user’s eligibility and if eligible, forwards the call to the implementation contract without costing the user any gas fees. Because we still want to use the underlying storage of the implementation contract, call will be used instead of delegatecall. We will have a dedicated article that explains in detail the difference between call, callcode, and delegatecall.

The proxy itself is extremely simple and straightforward. We will be making use of the fallback function and some ABI trickery to make it work with any smart contract. The fallback function is a special function that is called when the requested function signature cannot be found. It usually leads to a revert or STOP opcode but we can repurpose it to redirect the incoming call to our implementation contract.

The Execution

At this point, we really only need to accomplish 4 things:

  • Write a proxy contract using call
  • Enable ERC2771 compatibility
  • Verify user permissions
  • Finesse the ABI in JS

Writing a Permissioned Proxy

This part is very easy as we can make use of OpenZeppelin’s contract library. They provide a proxy preset that we can utilize right away:

import “@openzeppelin/contracts/proxy/Proxy.sol”;abstract contract AbstractProxy is Proxy { ... }

First, we would need to implement two virtual functions:

function _implementation() internal view virtual returns (address);function _beforeFallback() internal virtual {}

_implementation() is relatively straightforward as it simply returns the address of our implementation contract while _beforeFallback() is more interesting. As you can infer from the name, _beforeFallback() is called before the actual forwarding is executed and thus we can use it to determine if the function call should indeed be forwarded or not. We won’t cover what to write in this function since it completely differs from case to case, but here are a couple of ideas:

  • Check if the sender holds a certain NFT
  • Check if the sender is registered in a mapping

Then, we need to override and modify the following function:

function _delegate(address implementation) internal virtual { ... }

_delegate() is the function that handles the actual forwarding using Solidity assembly. You can view the same code with detailed comments in the GitHub repo but for the sake of formatting, I have stripped them from the snippets below.

By default it uses delegatecall:

assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}

As we mentioned earlier, we want to use call instead. This only requires a one-line change:

let result := call(gas(), implementation, callvalue(), 0, calldatasize(), 0, 0)

Note that we now have an extra callvalue() in the passed arguments.

One important caveat that must be kept in mind: msg.sender will no longer point to the intended caller. Instead, it always points to the proxy contract. We will cover this dynamic in depth in a separate article. There are various ways of mitigating this issue, one of which is to make use of ecrecover(), but it would be nice to see the addition of a new type of call that forwards msg.sender as calling ecrecover() also consumes some gas.

Although the solution isn’t completely plug-n-play after all, the advantage is not having to modify any existing external interfaces. Instead, we can add ones that are intended to be called by an “operator” on behalf of the sender, much like how it’s done in various token contracts, such as permit() in DAI.

Enabling ERC2771 Compliance

Integrating ERC2771 is very easy with OpenZeppelin. We simply have to import, inherit from the relevant module, and call the parent constructor:

import “@openzeppelin/contracts/metatx/ERC2771Context.sol”;abstract contract AbstractProxy is Proxy, ERC2771Context {    constructor(address trustedForwarder) ERC2771Context(trustedForwarder) { ... }}

ABI Trickery

While our proxy contract is now ready to go, there is a problem on the frontend. When we create an instance of a contract in web3.js or ethers.js, the framework reads from the ABI to determine the list of functions we are allowed to call. If we use the proxy ABI, we would only be allowed to call functions exposed in the proxy contract. But we want to call functions in the implementation contract and thus we should attach the implementation ABI to the proxy instance. This is completely okay because we implemented the fallback function and it properly forwards the call to the implementation contract, where the ABI will match.

Voila!

We now have created a permissioned proxy contract that enables us to pay gas fees for select users. The combination of ERC2771 and permissioned access certainly opens up a lot of possibilities as the underlying architecture of SignPass.

Next Steps

Very shortly we’ll be releasing a major batch of SignPass on BSC and Polygon, stay tuned for more updates! Early adopters, protocol partners, and investors of EthSign will be gifted with SignPass after this release, we thank you for your support along the way!

Access EthSign Here
Twitter | Gitbook | Discord | Youtube

--

--

Jack Boyuan Xu
Sign
Editor for

Co-founder & Tech Lead @ EthSign. Blockchain Lecturer @ USC.