diff --git a/contracts/interfaces/ICustomErrors.sol b/contracts/interfaces/ICustomErrors.sol index 9e6f16b4..ff90222c 100644 --- a/contracts/interfaces/ICustomErrors.sol +++ b/contracts/interfaces/ICustomErrors.sol @@ -8,9 +8,7 @@ interface ICustomErrors { error InconsistentParameter(string param); error InsufficientFunds(uint256 amount); error InternalMethod(); - error InvalidApprovalDataLength(); error InvalidCrowdfundBasisPointsFee(uint32 fee); - error InvalidPaymasterDataLength(); error InvalidPhase(); error NonExistentEntry(); error OutOfBounds(); diff --git a/contracts/paymaster/PaymasterTopFacet.sol b/contracts/paymaster/PaymasterTopFacet.sol index b4b6cbdc..a60ab407 100644 --- a/contracts/paymaster/PaymasterTopFacet.sol +++ b/contracts/paymaster/PaymasterTopFacet.sol @@ -1,15 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.10; -/** - * Note this is an "unwrapped" version of `BasePaymaster.sol` from the OpenGSN repo. - * Original license: GPL-3.0-only. - */ - -import "./lib/LibPaymaster.sol"; import "./lib/APaymasterFacet.sol"; +import "./lib/IPaymasterErrors.sol"; -import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "@opengsn/contracts/src/utils/GsnTypes.sol"; import "@opengsn/contracts/src/interfaces/IPaymaster.sol"; @@ -17,21 +12,16 @@ import "@opengsn/contracts/src/interfaces/IRelayHub.sol"; import "@opengsn/contracts/src/utils/GsnEip712Library.sol"; import "@opengsn/contracts/src/forwarder/IForwarder.sol"; -// import "hardhat/console.sol"; - -contract PaymasterTopFacet is IPaymaster { - using ERC165Checker for address; - - /// Errors. - // TODO: Move to ICustomErrors? - - error ApprovalDataNotEmpty(); - error ForwarderNotTrusted(address); - error InterfaceNotSupported(string); - error RelayHubAddressNotSet(); - error RequiresRelayHubCaller(); - error ValueTransferNotSupported(); +import "hardhat/console.sol"; +/** + * + * @notice The top-level Paymaster contract. + * Note this is an "unwrapped" version of `BasePaymaster.sol` from the OpenGSN repo. + * Original license: GPL-3.0-only. + * + */ +contract PaymasterTopFacet is APaymasterFacet, IPaymaster { IRelayHub internal relayHub; address private _trustedForwarder; @@ -60,6 +50,11 @@ contract PaymasterTopFacet is IPaymaster { return address(relayHub); } + /// @inheritdoc IPaymaster + function getTrustedForwarder() public view override(IPaymaster) returns (address) { + return _trustedForwarder; + } + /// @inheritdoc IPaymaster function getGasAndDataLimits() public pure override(IPaymaster) returns (IPaymaster.GasAndDataLimits memory limits) { return @@ -71,77 +66,30 @@ contract PaymasterTopFacet is IPaymaster { ); } + /// @inheritdoc IPaymaster /** - * @notice The owner of the Paymaster can change the instance of the RelayHub this Paymaster works with. - * :warning: **Warning** :warning: The deposit on the previous RelayHub must be withdrawn first. - */ - // TODO: Add MODIFIER - ONLY OWNER - function setRelayHub(IRelayHub hub) public { - if (!address(hub).supportsInterface(type(IRelayHub).interfaceId)) revert InterfaceNotSupported("IRelayHub"); - relayHub = hub; - } - - /** - * @notice The owner of the Paymaster can change the instance of the Forwarder this Paymaster works with. - * @notice the Recipients must trust this Forwarder as well in order for the configuration to remain functional. - */ - // TODO: Add MODIFIER - ONLY OWNER - function setTrustedForwarder(address forwarder) public { - if (!forwarder.supportsInterface(type(IForwarder).interfaceId)) revert InterfaceNotSupported("IForwarder"); - _trustedForwarder = forwarder; - } - - function getTrustedForwarder() public view override returns (address) { - return _trustedForwarder; - } - - /** - * @notice Any native Ether/MATIC transferred into the paymaster is transferred as a deposit to the RelayHub. - * This way, we don't need to understand the RelayHub API in order to replenish the paymaster. - */ - receive() external payable { - if (address(relayHub) == address(0)) revert RelayHubAddressNotSet(); - relayHub.depositFor{value: msg.value}(address(this)); - } - - /** - * @notice Withdraw deposit from the RelayHub. - * @param amount The amount to be subtracted from the sender. - * @param target The target to which the amount will be transferred. + * This method must be called from preRelayedCall to validate that the forwarder + * is approved by the paymaster as well as by the recipient contract. */ - // TODO: Add MODIFIER - ONLY OWNER - function withdrawRelayHubDepositTo(uint256 amount, address payable target) public { - relayHub.withdraw(target, amount); - } - - /// @inheritdoc IPaymaster function preRelayedCall( GsnTypes.RelayRequest calldata relayRequest, bytes calldata /* signature */, bytes calldata approvalData, uint256 /* maxPossibleGas */ ) external view override(IPaymaster) verifyRelayHubOnly returns (bytes memory, bool) { - /** - * This method must be called from preRelayedCall to validate that the forwarder - * is approved by the paymaster as well as by the recipient contract. - */ - if (getTrustedForwarder() != relayRequest.relayData.forwarder) - revert ForwarderNotTrusted(relayRequest.relayData.forwarder); - // TODO: Check what this calls... + revert IPaymasterErrors.ForwarderNotTrusted(relayRequest.relayData.forwarder); + + // SEE: `verifyForwarderTrusted`: GsnEip712Library.verifyForwarderTrusted(relayRequest); - if (relayRequest.request.value != 0) revert ValueTransferNotSupported(); - if (relayRequest.relayData.paymasterData.length != 0) revert ICustomErrors.InvalidPaymasterDataLength(); - if (approvalData.length != 0) revert ApprovalDataNotEmpty(); + if (relayRequest.request.value != 0) revert IPaymasterErrors.ValueTransferNotSupported(); + if (relayRequest.relayData.paymasterData.length != 0) revert IPaymasterErrors.InvalidPaymasterDataLength(); + if (approvalData.length != 0) revert IPaymasterErrors.InvalidApprovalDataLength(); /** - * Internal logic the paymasters need to provide to select which transactions they are willing to pay for - * see the documentation for `IPaymaster::preRelayedCall` for details + * See the documentation for `IPaymaster::preRelayedCall` for details. */ - if (approvalData.length == 0) revert ICustomErrors.InvalidApprovalDataLength(); - if (relayRequest.relayData.paymasterData.length == 0) revert ICustomErrors.InvalidPaymasterDataLength(); - return ("", useRejectOnRecipientRevert); } @@ -159,14 +107,58 @@ contract PaymasterTopFacet is IPaymaster { (context, success, gasUseWithoutPost, relayData); } + /// @inheritdoc IPaymaster function versionPaymaster() external pure override(IPaymaster) returns (string memory) { return "3.0.0-beta.9+opengsn.tokensphere.ipaymaster"; } + // Setters and utility methods. + + /** + * @notice The owner of the Paymaster can change the instance of the RelayHub this Paymaster works with. + * :warning: **Warning** :warning: The deposit on the previous RelayHub must be withdrawn first. + */ + // TODO: Add MODIFIER - ONLY OWNER + function setRelayHub(IRelayHub hub) public { + if (!IERC165(address(hub)).supportsInterface(type(IRelayHub).interfaceId)) + revert IPaymasterErrors.InterfaceNotSupported("IRelayHub"); + relayHub = hub; + } + + /** + * @notice The owner of the Paymaster can change the instance of the Forwarder this Paymaster works with. + * @notice the Recipients must trust this Forwarder as well in order for the configuration to remain functional. + */ + // TODO: Add MODIFIER - ONLY OWNER + function setTrustedForwarder(address forwarder) public { + if (!IERC165(forwarder).supportsInterface(type(IForwarder).interfaceId)) + revert IPaymasterErrors.InterfaceNotSupported("IForwarder"); + _trustedForwarder = forwarder; + } + + /** + * @notice Any native Ether/MATIC transferred into the Paymaster is transferred as a deposit to the RelayHub. + * This way, we don't need to understand the RelayHub API in order to replenish the paymaster. + */ + function deposit() external payable { + if (address(relayHub) == address(0)) revert IPaymasterErrors.RelayHubAddressNotSet(); + relayHub.depositFor{value: msg.value}(address(this)); + } + + /** + * @notice Withdraw deposit from the RelayHub. + * @param amount The amount to be subtracted from the sender. + * @param target The target to which the amount will be transferred. + */ + // TODO: Add MODIFIER - ONLY OWNER + function withdrawRelayHubDepositTo(uint256 amount, address payable target) public { + relayHub.withdraw(target, amount); + } + /// Modifiers. modifier verifyRelayHubOnly() virtual { - if (msg.sender != getRelayHub()) revert RequiresRelayHubCaller(); + if (msg.sender != getRelayHub()) revert IPaymasterErrors.RequiresRelayHubCaller(); _; } } diff --git a/contracts/paymaster/lib/APaymasterFacet.sol b/contracts/paymaster/lib/APaymasterFacet.sol index 7878f904..6213cb47 100644 --- a/contracts/paymaster/lib/APaymasterFacet.sol +++ b/contracts/paymaster/lib/APaymasterFacet.sol @@ -1,10 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.10; -import "../../common/AHasMembers.sol"; -import "../../interfaces/ICustomErrors.sol"; +import "./LibPaymaster.sol"; import "../../lib/LibHelpers.sol"; -import "../lib/LibPaymaster.sol"; +import "../../interfaces/ICustomErrors.sol"; /** * @notice This contract is a group of modifiers that can be used by any Paymaster facets to guard against diff --git a/contracts/paymaster/lib/IPaymasterErrors.sol b/contracts/paymaster/lib/IPaymasterErrors.sol new file mode 100644 index 00000000..e5dac903 --- /dev/null +++ b/contracts/paymaster/lib/IPaymasterErrors.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +/** + * @notice Errors that can occur within the paymaster. + */ +interface IPaymasterErrors { + error ForwarderNotTrusted(address); + error InterfaceNotSupported(string); + error InvalidPaymasterDataLength(); + error InvalidApprovalDataLength(); + error RelayHubAddressNotSet(); + error RequiresRelayHubCaller(); + error ValueTransferNotSupported(); +} diff --git a/hardhat.config.ts b/hardhat.config.ts index e1b73a0f..188851c8 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -127,8 +127,7 @@ const config: HardhatUserConfig = { // Event types. // ... // Error types. - ["Facet$", "InvalidApprovalDataLength()"], - ["Facet$", "InvalidPaymasterDataLength()"], + // ... ]), include: [ "IERC173", diff --git a/test/fixtures/paymaster.ts b/test/fixtures/paymaster.ts new file mode 100644 index 00000000..1375db21 --- /dev/null +++ b/test/fixtures/paymaster.ts @@ -0,0 +1,79 @@ +import { ethers } from "hardhat"; +import { MockContract } from "@defi-wonderland/smock"; +import { FixtureFunc } from "hardhat-deploy/dist/types"; +import { deploymentSalt, ZERO_ADDRESS } from "../../src/utils"; +import { facetMock } from "../utils"; +import { + PaymasterTopFacet, + PaymasterInitFacet, + PaymasterTopFacet__factory, +} from "../../typechain"; +import { PAYMASTER_FACETS } from "../../tasks/paymaster"; +import { Paymaster } from "../../typechain/hardhat-diamond-abi/HardhatDiamondABI.sol"; + +export const PAYMASTER_INIT_DEFAULTS: PaymasterInitFacet.InitializerParamsStruct = +{ + marketplace: ZERO_ADDRESS, +}; + +interface PaymasterFixtureResult { + paymaster: Paymaster; + topMock: MockContract; +} +interface PaymasterFixtureOpts { + readonly name: string; + readonly deployer: string; + readonly afterDeploy: (result: PaymasterFixtureResult) => void; +} +interface PaymasterFixtureFuncArgs { + readonly initWith: {}; + readonly opts: PaymasterFixtureOpts; +} + +export const paymasterFixtureFunc: FixtureFunc< + PaymasterFixtureResult, + PaymasterFixtureFuncArgs +> = async (hre, opts) => { + // opts could be `undefined`. + if (!opts) throw Error("You must provide Paymaster fixture options."); + const { + opts: { deployer, name, afterDeploy }, + initWith, + } = opts; + // Deploy diamond. + const { address: paymasterAddr } = await hre.deployments.diamond.deploy( + name, + { + from: deployer, + owner: deployer, + facets: [...PAYMASTER_FACETS, "PaymasterInitFacet"], + execute: { + contract: "PaymasterInitFacet", + methodName: "initialize", + args: [{ ...PAYMASTER_INIT_DEFAULTS, ...initWith }], + }, + deterministicSalt: deploymentSalt(hre), + excludeSelectors: { + "PaymasterTopFacet": ["supportsInterface"] + } + } + ); + + // Get a Paymaster typed pointer. + const paymaster = await ethers.getContractAt( + "Paymaster", + paymasterAddr + ); + // Build result. + const result: PaymasterFixtureResult = { + paymaster, + topMock: await facetMock( + paymaster, + "PaymasterTopFacet" + ), + }; + // Callback! + await afterDeploy.apply(this, [result]); + // Final return. + return result; +}; diff --git a/test/paymaster/PaymasterInitFacet.test.ts b/test/paymaster/PaymasterInitFacet.test.ts new file mode 100644 index 00000000..b00bc584 --- /dev/null +++ b/test/paymaster/PaymasterInitFacet.test.ts @@ -0,0 +1,114 @@ +import * as chai from "chai"; +import { expect } from "chai"; +import { solidity } from "ethereum-waffle"; +import { deployments, ethers } from "hardhat"; +import { FakeContract, smock } from "@defi-wonderland/smock"; +import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; +import { PaymasterTopFacet, PaymasterInitFacet } from "../../typechain"; +import { paymasterFixtureFunc } from "../fixtures/paymaster"; +import { BigNumber } from "ethers"; +import { impersonateContract } from "../utils"; +import { DEPLOYER_FACTORY_COMMON } from "../../src/utils"; +import { + Marketplace, + Paymaster, +} from "../../typechain/hardhat-diamond-abi/HardhatDiamondABI.sol"; +chai.use(solidity); +chai.use(smock.matchers); + +describe("PaymasterInitFacet", () => { + let deployer: SignerWithAddress; + let marketplace: FakeContract, + paymaster: Paymaster, + top: PaymasterTopFacet; + + const paymasterDeployFixture = deployments.createFixture( + paymasterFixtureFunc + ); + + before(async () => { + // Keep track of a few signers. + [deployer] = await ethers.getSigners(); + // Mock a Marketplace contract. + marketplace = await smock.fake("Marketplace"); + }); + + beforeEach(async () => { + await paymasterDeployFixture({ + opts: { + name: "PaymasterInitFixture", + deployer: deployer.address, + afterDeploy: async (args) => { + ({ paymaster } = args); + top = await ethers.getContractAt( + "PaymasterTopFacet", + paymaster.address + ); + }, + }, + initWith: { + marketplace: marketplace.address, + }, + }); + }); + + describe("initialize", () => { + it("requires that it is not initialized", async () => { + // Attempt to re-initialize. + const paymasterInit = await ethers.getContractAt( + "PaymasterInitFacet", + paymaster.address + ); + const paymasterInitAsItself = await impersonateContract( + paymasterInit, + DEPLOYER_FACTORY_COMMON.factory + ); + const subject = paymasterInitAsItself.initialize({ + marketplace: marketplace.address, + }); + + await expect(subject).to.have.revertedWith("AlreadyInitialized"); + }); + + it("set various storage versions", async () => { + // Query the slot and parse out the STORAGE_VERSION. + const slot = ethers.utils.solidityKeccak256( + ["string"], + ["Paymaster.storage"] + ); + const data = await ethers.provider.send("eth_getStorageAt", [ + paymaster.address, + slot, + "latest", + ]); + // Slice out the final 2 bytes to get the version. + const subject = ethers.utils.hexDataSlice(data, 30, 32); + + // Expectations. + expect(BigNumber.from(subject).toString()).to.eq("1"); + }); + + it("registers supported interfaces", async () => { + expect({ + IERC165: await paymaster.supportsInterface("0x01ffc9a7"), + IERC173: await paymaster.supportsInterface("0x7f5828d0"), + IDiamondCut: await paymaster.supportsInterface("0x1f931c1c"), + IDiamondLoupe: await paymaster.supportsInterface("0x48e2b093"), + IPaymaster: await paymaster.supportsInterface("0xe1ab2dea"), + }).to.be.eql({ + IERC165: true, + IERC173: true, + IDiamondCut: true, + IDiamondLoupe: true, + IPaymaster: true, + }); + }); + + it("stores the given Marketplace address") + //, async () => { + // Querying the Marketplace address via the PaymasterTopFacet should return the stored address. + // const subject = await top.marketplaceAddress(); + // expect(subject).to.be.eq(marketplace.address); + // }); + }); +}); diff --git a/test/paymaster/PaymasterTopFacet.test.ts b/test/paymaster/PaymasterTopFacet.test.ts new file mode 100644 index 00000000..9665ada2 --- /dev/null +++ b/test/paymaster/PaymasterTopFacet.test.ts @@ -0,0 +1,332 @@ +import * as chai from "chai"; +import { expect } from "chai"; +import { solidity } from "ethereum-waffle"; +import { BigNumber } from "ethers"; +import { deployments, ethers } from "hardhat"; +import { FakeContract, smock } from "@defi-wonderland/smock"; +import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; +import { abiStructToObj, impersonateContract, stopImpersonating } from "../utils"; +import { paymasterFixtureFunc } from "../fixtures/paymaster"; +import { + Issuer, + Marketplace, + Paymaster, +} from "../../typechain/hardhat-diamond-abi/HardhatDiamondABI.sol"; +import { + IForwarder, + IRelayHub, + PaymasterTopFacet +} from "../../typechain"; +import { GsnTypes } from "@opengsn/contracts/dist/types/ethers-contracts/ArbRelayHub"; +import { formatEther } from "ethers/lib/utils"; +chai.use(solidity); +chai.use(smock.matchers); + +describe("PaymasterTopFacet", () => { + let deployer: SignerWithAddress, + issuerMember: SignerWithAddress, + alice: SignerWithAddress; + let issuer: FakeContract, + marketplace: FakeContract, + forwarder: FakeContract, + relayHub: FakeContract, + paymaster: Paymaster, + alicePaymaster: PaymasterTopFacet; + + const paymasterDeployFixture = deployments.createFixture( + paymasterFixtureFunc + ); + + before(async () => { + // Keep track of a few signers. + [deployer, issuerMember, alice] = await ethers.getSigners(); + // Mock contracts. + issuer = await smock.fake("Issuer"); + marketplace = await smock.fake("Marketplace"); + + forwarder = await smock.fake("IForwarder"); + relayHub = await smock.fake("IRelayHub"); + }); + + beforeEach(async () => { + await paymasterDeployFixture({ + opts: { + name: "PaymasterTopFixture", + deployer: deployer.address, + afterDeploy: async (args) => { + ({ paymaster } = args); + + alicePaymaster = await paymaster.connect(alice); + }, + }, + initWith: { + marketplace: marketplace.address, + }, + }); + + // IERC165. + + forwarder.supportsInterface.reset(); + // IForwarder interface. + forwarder.supportsInterface + .whenCalledWith("0x25e23e64") + .returns(true); + forwarder.supportsInterface.returns(false); + + relayHub.supportsInterface.reset(); + // IRelayHub interface. + relayHub.supportsInterface + .whenCalledWith("0xe9fb30f7") + .returns(true); + relayHub.supportsInterface.returns(false); + + relayHub.depositFor.reset(); + }); + + // IERC165. + + describe("IERC165", () => { + describe("supportsInterface", () => { + it("registers supported interfaces") + // , async () => { + // await paymaster.supportsInterface("0x01ffc9a7") + + // }); + }); + }); + + // IPaymaster. + + describe("IPaymaster", () => { + + describe("getRelayHub", () => { + it("returns the relay hub address", async () => { + // Set the relay hub address. + await alicePaymaster.setRelayHub(relayHub.address); + + const subject = await paymaster.getRelayHub(); + expect(subject).to.eq(relayHub.address); + }); + }); + + describe("getTrustedForwarder", () => { + it("returns the trusted forwarder address", async () => { + // Set the trusted forwarder. + await alicePaymaster.setTrustedForwarder(forwarder.address); + + const subject = await paymaster.getTrustedForwarder(); + expect(subject).to.eq(forwarder.address); + }); + }); + + describe("getGasAndDataLimits", () => { + // SEE: https://github.com/opengsn/gsn/blob/master/packages/contracts/src/BasePaymaster.sol + it("returns the gas and data limits", async () => { + const subject = abiStructToObj(await paymaster.getGasAndDataLimits()); + + expect(subject).to.eql({ + acceptanceBudget: BigNumber.from(100000 + 50000), + preRelayedCallGasLimit: BigNumber.from(100000), + postRelayedCallGasLimit: BigNumber.from(110000), + calldataSizeLimit: BigNumber.from(10500), + }); + }); + }); + + describe("preRelayedCall", () => { + let stubbedRequest = {}; + let stubbedRelayData = {}; + + beforeEach(async () => { + stubbedRequest = { + from: issuerMember.address, + to: issuer.address, + value: 0, + gas: 15000, + nonce: 0, + data: "0x", + validUntilTime: 0, + }; + + stubbedRelayData = { + maxFeePerGas: 0, + maxPriorityFeePerGas: 0, + transactionCalldataGasUsed: 0, + relayWorker: alice.address, + paymaster: paymaster.address, + forwarder: forwarder.address, + paymasterData: "0x", + clientId: 0, + }; + }); + + // SEE: https://github.com/opengsn/gsn/blob/master/packages/contracts/src/BasePaymaster.sol + it("reverts if not permissioned"); + + it("reverts if not called by the relay hub", async () => { + const signature = "0x"; + const approvalData = "0x"; + const maxPossibleGas = 0; + + const subject = paymaster.preRelayedCall({ + request: stubbedRequest, + relayData: stubbedRelayData, + }, signature, approvalData, maxPossibleGas); + + expect(subject).to.be.revertedWith("RequiresRelayHubCaller"); + }); + + describe("when called by the relay hub", () => { + let relayHubCaller: PaymasterTopFacet; + + // Defaults. + const signature = "0x"; + const approvalData = "0x"; + const maxPossibleGas = 0; + + beforeEach(async () => { + alicePaymaster.setRelayHub(relayHub.address); + + await ethers.provider.send("hardhat_impersonateAccount", [relayHub.address]); + relayHubCaller = paymaster.connect(await ethers.getSigner(relayHub.address)); + }); + + afterEach(async () => await stopImpersonating(relayHub.address)); + + it("reverts if the forwarder is not trusted", async () => { + const subject = relayHubCaller.preRelayedCall({ + request: stubbedRequest, + relayData: stubbedRelayData, + }, signature, approvalData, maxPossibleGas); + + expect(subject).to.be.revertedWith("ForwarderNotTrusted"); + }); + + it("reverts if the value is not zero", async () => { + issuer.isTrustedForwarder.reset(); + issuer.isTrustedForwarder.returns(true); + alicePaymaster.setTrustedForwarder(forwarder.address); + + const subject = relayHubCaller.preRelayedCall({ + request: { ...stubbedRequest, value: 1 }, + relayData: stubbedRelayData, + }, signature, approvalData, maxPossibleGas); + + expect(subject).to.be.revertedWith("ValueTransferNotSupported"); + }); + + it("reverts if there is paymaster data", async () => { + issuer.isTrustedForwarder.reset(); + issuer.isTrustedForwarder.returns(true); + alicePaymaster.setTrustedForwarder(forwarder.address); + + const subject = relayHubCaller.preRelayedCall({ + request: stubbedRequest, + relayData: { ...stubbedRelayData, paymasterData: "0xdeadbeef" }, + }, signature, approvalData, maxPossibleGas); + + expect(subject).to.be.revertedWith("InvalidPaymasterDataLength"); + }); + + it("reverts if there is approval data", async () => { + issuer.isTrustedForwarder.reset(); + issuer.isTrustedForwarder.returns(true); + alicePaymaster.setTrustedForwarder(forwarder.address); + + const subject = relayHubCaller.preRelayedCall({ + request: stubbedRequest, + relayData: stubbedRelayData, + }, signature, /* approvalData */ "0xdeadbeef", maxPossibleGas); + + expect(subject).to.be.revertedWith("InvalidApprovalDataLength"); + }); + + it("on success returns empty string and boolean", async () => { + issuer.isTrustedForwarder.reset(); + issuer.isTrustedForwarder.returns(true); + alicePaymaster.setTrustedForwarder(forwarder.address); + + const subject = await relayHubCaller.preRelayedCall({ + request: stubbedRequest, + relayData: stubbedRelayData, + }, signature, approvalData, maxPossibleGas); + + expect(subject).to.eql(["0x", false]); + }); + }); + }); + + describe("postRelayedCall", () => { + it("reverts if not permissioned"); + it("reverts if not called by the relay hub"); + }); + + describe("versionPaymaster", () => { + it("returns the paymaster semver string", async () => { + const subject = await paymaster.versionPaymaster(); + expect(subject).to.eq("3.0.0-beta.9+opengsn.tokensphere.ipaymaster"); + }); + }); + }); + + // Settings and utility functions. + + describe("setRelayHub", () => { + it("reverts if not permissioned"); + + it("requires the address to be a valid relay hub", async () => { + // Attempt to add the Issuer as the relay hub. + const subject = alicePaymaster.setRelayHub(issuer.address); + expect(subject).to.be.revertedWith("InterfaceNotSupported"); + }); + + it("sets the relay hub address", async () => { + await alicePaymaster.setRelayHub(relayHub.address); + const subject = await alicePaymaster.getRelayHub(); + expect(subject).to.eq(relayHub.address); + }); + }); + + describe("setTrustedForwarder", () => { + it("reverts if not permissioned"); + + it("requires the address to be a valid forwarder", async () => { + const subject = alicePaymaster.setTrustedForwarder(issuer.address); + expect(subject).to.be.revertedWith("InterfaceNotSupported"); + }); + + it("sets the trusted forwarder", async () => { + await alicePaymaster.setTrustedForwarder(forwarder.address); + const subject = await alicePaymaster.getTrustedForwarder(); + expect(subject).to.eq(forwarder.address); + }); + }); + + describe("deposit", () => { + it("reverts if Relay hub address not set", async () => { + const subject = alicePaymaster.deposit({ value: 100 }); + expect(subject).to.be.revertedWith("RelayHubAddressNotSet"); + }); + + it("deposits sent amount to relay hub", async () => { + // Firstly set the relay hub address. + await paymaster.setRelayHub(relayHub.address); + + await alicePaymaster.deposit({ value: 100 }); + expect(relayHub.depositFor).to.have.been.calledWith( + paymaster.address + ); + }); + }); + + describe("withdrawRelayHubDepositTo", () => { + it("reverts if not permissioned"); + + it("withdraws the relay hub deposit to the given address", async () => { + // Set the relay hub. + await paymaster.setRelayHub(relayHub.address); + + await alicePaymaster.withdrawRelayHubDepositTo(100, alice.address); + }); + }); +});