Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Features: Gasless transactions. #89

Merged
merged 31 commits into from
Apr 4, 2024

Conversation

jamesduncombe
Copy link
Contributor

@jamesduncombe jamesduncombe commented Oct 19, 2023

🚧 WIP 🚧

Known issues to fix:

  • Actually securing the functions... modifiers etc 💥
  • Working out how to deal with testing locally (the RelayHub etc).
  • Fixing and static stubbed values (contract addresses etc).
  • Tests in general.
  • supportsInterface forced by IPaymaster interface... but this is on the facet. 💥

OpenGSN ref: https://docs.opengsn.org/contracts/#paying-for-your-user-s-meta-transaction
BasePaymaster.sol: https://github.com/opengsn/gsn/blob/master/packages/contracts/src/BasePaymaster.sol


Notes

Currently includes...

  • AHasContext - Implementation for _isTrustedForwarder, _msgSender() and _msgData(). Used to override base classes.
  • AHasForwarder - Implementation of the Forwardable behaviour. Should be inherited from when adding a "Forwardable" facet. Adds setters, getters, events and errors.
  • LibHasForwarder - Diamond storage for AHasForwarder.
  • AHasMembers - updated to point to _msgSender(), provides default implementation which points to msg.sender - i.e. it's not aware of anything forwardable etc.
  • Concrete facet example: MarketplaceAccessFacet - Inherits from AHasMembers and AHasContext.
  • Concrete facet example: MarketplaceForwardableFacet - Inherits from AHasForwarder and AHasContext.

MarketplaceAccessFacet example:

contract MarketplaceAccessFacet is AMarketplaceFacet, AHasMembers, AHasContext, IHasActiveMembers {

...

  /// AHasContext implementation.

  // Delegate to Forwardable Marketplace facet.
  function _isTrustedForwarder(address forwarder) internal view override(AHasContext) returns (bool) {
    return AHasForwarder(address(this)).isTrustedForwarder(forwarder);
  }
  
  // Delegate to default implementation.
  function _msgSender() internal view override(AHasMembers, AHasContext) returns (address) {
    return AHasContext._msgSender();
  }

...
}

@shortcut-integration
Copy link

This pull request has been linked to Shortcut Story #9185: Gasless transactions.

@jamesduncombe jamesduncombe changed the title 9185: Features: gasless transactions 9185: Features: Gasless transactions. Oct 19, 2023
@jamesduncombe jamesduncombe added the enhancement New feature or request label Oct 19, 2023
@jamesduncombe jamesduncombe force-pushed the features/sc-9185-gasless-transactions branch 2 times, most recently from 4ef9d74 to 44c55a8 Compare October 19, 2023 17:54
@jamesduncombe jamesduncombe marked this pull request as draft October 19, 2023 17:55
@jamesduncombe jamesduncombe force-pushed the features/sc-9185-gasless-transactions branch from 8b3a932 to ccca60d Compare October 23, 2023 17:01
@codecov
Copy link

codecov bot commented Oct 23, 2023

Codecov Report

Attention: Patch coverage is 91.79104% with 11 lines in your changes are missing coverage. Please review.

Project coverage is 79.03%. Comparing base (fd3cf39) to head (588a177).
Report is 7 commits behind head on main.

Files Patch % Lines
contracts/common/AHasContext.sol 55.55% 4 Missing ⚠️
contracts/common/AHasGovernors.sol 50.00% 2 Missing ⚠️
contracts/fast/FastForwardableFacet.sol 0.00% 2 Missing ⚠️
contracts/paymaster/PaymasterTopFacet.sol 95.65% 2 Missing ⚠️
contracts/common/AHasForwarder.sol 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            main      #89       +/-   ##
==========================================
+ Coverage   7.52%   79.03%   +71.50%     
==========================================
  Files         47       56        +9     
  Lines        797     1116      +319     
  Branches     210      234       +24     
==========================================
+ Hits          60      882      +822     
+ Misses       737      234      -503     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

}
// Directly import IERC165 from OpenZeppelin contracts.
// Solves an issue with 2 conflicting definitions of IERC165.
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice reusing something from libs here.

// The current version of the storage.
uint16 internal constant STORAGE_VERSION = 1;
// This is `keccak256('HasForwarder.storage.Main')`.
bytes32 internal constant STORAGE_SLOT = 0xa9930c2ffa1b605b0243ba36b3020146bcba5a29c05a711f5ca7c705a8e851ca;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct.

// ------------------------------------- //

// Initialize forwarder storage.
LibHasForwarder.data().forwarderAddress = params.trustedForwarder;
Copy link
Member

@hickscorp hickscorp Oct 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how this will work in production.
Adding to the initialize function will work locally or when deploying the Smart Contract from scratch...
But in production you can't really call initialize again.
Maybe what you could do is add a function to the InitFacet such as migrateV1ToV2 and add the code there?

Then your initialize method could call migrateV1ToV2, and in production we would simply call migrateV1ToV2 once the facet is upgraded.

This leaves with the modifiers question. migrateV1ToV2 should probably allow for only the deployer to call it? I think it would cover both cases... I'm not 100% sure on this one.

abstract contract BasePaymaster is IPaymaster, ERC165 {
using ERC165Checker for address;

IRelayHub internal relayHub;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

}

//overhead of forwarder verify+signature, plus hub overhead.
uint256 public constant FORWARDER_HUB_OVERHEAD = 50000;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are those the real costs? I thought it was much cheaper to go with this pattern. Oh well.

Copy link
Contributor Author

@jamesduncombe jamesduncombe Oct 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything in BasePaymaster.sol is copied from their version: https://github.com/opengsn/gsn/blob/master/packages/contracts/src/BasePaymaster.sol. Why is it copied? Their version uses Ownable from OZ which I didn't want to bring in.

OpenGSN's implementation instructions say to grab BasePaymaster.sol and base our base paymaster from that. See here for sample paymaster contracts: https://github.com/opengsn/gsn/tree/master/packages/paymasters/contracts

Ideally I wouldn't copy this in...

* - preRelayedCall
* - postRelayedCall
*/
abstract contract BasePaymaster is IPaymaster, ERC165 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this contract won't ever be upgraded, correct?


/// @inheritdoc IERC165
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) {
return interfaceId == type(IPaymaster).interfaceId || super.supportsInterface(interfaceId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not 100% sure what super refers to here... Is it ERC165? Let's just make sure that whichever parent gets called, it also has supportsInterface(bytes4)...

}

function _verifyRelayHubOnly() internal view virtual {
require(msg.sender == getRelayHub(), "can only be called by RelayHub");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest that all these require are changed to if statements and proper revert exception primitives.
They give you more flexibility for tests (you can have exception take known arguments, and pass these as informations to the client or the test).

@@ -6,7 +6,10 @@ import { deployments } from "hardhat";
const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am strongly against changing migrations that have been followed to production.
The outcome of doing so is an environment during tests that differs from production.
So all your tests might pass - but won't be testing what's in production.

In this case, you're deploying a Marketplace contract that has an extra facet in test environment...

So unless you make absolutelly 100% sure that you have a migration function to bring production to the same state, it's tricky.

How about having a migration which reflects the steps that will need to be taken in production, for example calling an upgrade and a separate migration function such as migrateV1toV2?

// TODO: From a lookup...
const trustedForwarderAddr = "0xB2b5841DBeF766d4b521221732F9B618fCf34A87";

await deployMarketplace(hre, (await deployments.get("Issuer")).address, trustedForwarderAddr);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big red flag here...
Deterministic deploys via factories hash the arguments.
It means that a Marketplace contract with a different trustedForwarderAddr would get deployed to a completelly different address than another.

What I would suggest is the following:

  • Have the Marketplace deployment not take an extra argument for trustedForwarderAddr.
  • Internally, the trustedForwarderAddr would be set to 0x0.

At this point you still have deterministic deployments.

Then:

  • Account for trustedForwarderAddr being 0x0 in the Marketplace code - so that it won't function until it's set.
  • Have a method to change trustedForwarderAddr (Which I believe I've seen)...

Or am I missing something?

@@ -0,0 +1,12 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, justified - new contract, new migration.

@@ -20,29 +20,30 @@
},
"dependencies": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hell. Good job.

package.json Outdated
@@ -20,29 +20,30 @@
},
"dependencies": {
"@openzeppelin/contracts": "^4.9.2",
"@typechain/ethers-v5": "^11.0.0",
"@typechain/hardhat": "^8.0.0",
"@opengsn/contracts": "^3.0.0-beta.6",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure about this? Them being in beta means not audited yet - no?
I'd either wait for 3.0.0 to be a thing, or use 2.0.0 unless there are major changes between the two.
We could discuss this one.

Copy link
Member

@hickscorp hickscorp Oct 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example... They're already at 3.0.0-beta.9 (published) and 3.0.0-beta.9 (unpublished). I'd be a bit scared to go for a production deploy with a beta.


task("marketplace-deploy", "Deploys the main Marketplace contract").setAction(
async (_params: MarketplaceDeployParams, hre) => {
const { address: issuerAddr } = await hre.deployments.get("Issuer");
await deployMarketplace(hre, issuerAddr);
// TODO: From a lookup...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What lookup? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's yet to exist :) - this was a note-to-self to add one... the Relays are already deployed on the chain but we need to add a lookup (or similar) for their addresses.


interface PaymasterFundParams { }

task("paymaster-fund", "Funds the Paymaster")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job sticking to the weird naming convention I chose...
My logic was that I wanted tasks groupped by contract. Contract name first, task name after. I know it's weird, but it makes it easier to find what you're looking for...

const { address: paymasterAddress } = await deployments.get("Paymaster");

const RelayHub = await hre.ethers.getContractFactory("RelayHub");
const relayHub = await RelayHub.attach(relayHubAddress);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not directly const relayHub = await hre.ethers.getContractAt(...)?


// params...
const tx = await relayHub.depositFor(paymasterAddress, { value: parseEther("0.1") });
await tx.wait();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep.

const { deployer } = await getNamedAccounts();

let deploy = await deployments.getOrNull("Paymaster");
if (deploy) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good job conserving the style of "check if it's done first".
You will see that it will save you a lot of time when a deployment fails midway.

@@ -44,7 +44,7 @@ describe("Crowdfunds", () => {
issuer = await smock.fake("Issuer");
marketplace = await smock.fake("Marketplace");
fast = await smock.fake("Fast");
erc20 = await smock.fake("IERC20");
erc20 = await smock.fake("contracts/interfaces/IERC20.sol:IERC20");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait. What?

Copy link
Contributor Author

@jamesduncombe jamesduncombe Oct 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, super annoying... example of what happens without this:

  4) FastDistributionsFacet
       "before all" hook in "FastDistributionsFacet":
     Error: unable to generate smock spec from contract name.
HH701: There are multiple artifacts for contract "IERC20", please use a fully qualified name.

Please replace IERC20 for one of these options wherever you are trying to read its artifact:

@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20
contracts/interfaces/IERC20.sol:IERC20

My understanding of this is that in the artifacts, there are essentially two IERC20 definitions so it can't work it out.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hold on. If it picked up the OZ version, it means that something more concerning is going on. It could mean that a contract you use from OZ is referencing its own ERC20 interface.
Otherwise it wouldn't get built and artifacts generated.

I would aim at having a single source of truth here - in case something changes in our ERC20 or theirs, it could lead to something nasty.

Don't worry about this one though - it's a tooling thing we can resolve in different ways.

Copy link
Contributor Author

@jamesduncombe jamesduncombe Oct 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in a4797ed for now...

OZ's is referened within IStakeManager (referenced from IRelayHub) within Open GSN.

@jamesduncombe jamesduncombe force-pushed the features/sc-9185-gasless-transactions branch 3 times, most recently from 5f6dcc8 to e20c81e Compare October 25, 2023 17:22
@jamesduncombe jamesduncombe force-pushed the features/sc-9185-gasless-transactions branch from 618d4eb to bdaf271 Compare April 3, 2024 09:29
@jamesduncombe jamesduncombe marked this pull request as ready for review April 3, 2024 09:37
@jamesduncombe jamesduncombe merged commit 25f3695 into main Apr 4, 2024
4 checks passed
@jamesduncombe jamesduncombe deleted the features/sc-9185-gasless-transactions branch April 4, 2024 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants