From d30c62f88a6316b560d9b9f86a932b8a9d9fae77 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Thu, 8 Sep 2022 22:24:41 -0400 Subject: [PATCH 01/30] fix(hardhat-config): add solidity 0.8.15 compiler --- hardhat.config.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index a5ccc964a..c075665df 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -104,6 +104,22 @@ const config: HardhatUserConfig = { }, }, }, + { + version: "0.8.15", + settings: { + metadata: { + // Not including the metadata hash + // https://github.com/paulrberg/solidity-template/issues/31 + bytecodeHash: "none", + }, + // Disable the optimizer when debugging + // https://hardhat.org/hardhat-network/#solidity-optimizer-support + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, ], }, etherscan: { @@ -169,7 +185,7 @@ const config: HardhatUserConfig = { initialBaseFeePerGas: 1_00_000_000, gasPrice: "auto", forking: buildForkConfig(FORK as eEVMNetwork, FORK_BLOCK_NUMBER), - allowUnlimitedContractSize: false, + allowUnlimitedContractSize: true, chainId: NETWORKS_CHAIN_ID[NETWORK_NAME as eEVMNetwork], accounts: { mnemonic, From 0e9eb0dc58b7b9f8895893743f2f0912dac85b6f Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Thu, 8 Sep 2022 22:30:40 -0400 Subject: [PATCH 02/30] fix(utils): meta-tx sign util --- test/test-opty/utils.ts | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/test/test-opty/utils.ts b/test/test-opty/utils.ts index b860302c4..c05b2c620 100644 --- a/test/test-opty/utils.ts +++ b/test/test-opty/utils.ts @@ -278,3 +278,59 @@ export async function getPermitSignature( ), ); } + +const EIP712Domain = [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, +]; + +const ForwardRequest = [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "gas", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "data", type: "bytes" }, +]; + +function getMetaTxTypeData(chainId: number, verifyingContract: string) { + return { + types: { + EIP712Domain, + ForwardRequest, + }, + domain: { + name: "MinimalForwarder", + version: "0.0.1", + chainId, + verifyingContract, + }, + primaryType: "ForwardRequest", + }; +} + +async function signTypedData(signer: any, from: string, data: any) { + const isHardhat = data.domain.chainId == 31337; + const [method, argData] = isHardhat ? ["eth_signTypedData", data] : ["eth_signTypedData_v4", JSON.stringify(data)]; + return await signer.send(method, [from, argData]); +} + +async function buildRequest(forwarder: Contract, input: any) { + const nonce = (await forwarder.getNonce(input.from)).toString(); + return { value: 0, gas: 1e6, nonce, ...input }; +} + +async function buildTypedData(forwarder: Contract, request: any) { + const chainId = (await forwarder.provider.getNetwork()).chainId; + const typeData = getMetaTxTypeData(chainId, forwarder.address); + return { ...typeData, message: request }; +} + +export async function signMetaTxRequest(signer: any, forwarder: Contract, input: any) { + const request = await buildRequest(forwarder, input); + const toSign = await buildTypedData(forwarder, request); + const signature = signTypedData(signer, input.from, toSign); + return { signature, request }; +} From 60e0b18539f132296e6bcf122c1628846264a48f Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Thu, 8 Sep 2022 22:31:46 -0400 Subject: [PATCH 03/30] feat(meta-vault): add meta vault contract --- contracts/protocol/meta-tx/MetaVault.sol | 94 ++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 contracts/protocol/meta-tx/MetaVault.sol diff --git a/contracts/protocol/meta-tx/MetaVault.sol b/contracts/protocol/meta-tx/MetaVault.sol new file mode 100644 index 000000000..406957f21 --- /dev/null +++ b/contracts/protocol/meta-tx/MetaVault.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import "@openzeppelin/contracts-0.8.x/metatx/ERC2771Context.sol"; +import "@openzeppelin/contracts-0.8.x/metatx/MinimalForwarder.sol"; +import "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts-0.8.x/token/ERC20/extensions/draft-IERC20Permit.sol"; + +interface IERC20PermitLegacy { + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} + +interface IVault { + /** + * @notice Deposit underlying tokens to the vault + * @dev Mint the shares right away as per oracle based price per full share value + * @param _beneficiary the address of the deposit beneficiary, + * if _beneficiary = address(0) => _beneficiary = msg.sender + * @param _userDepositUT Amount in underlying token + * @param _permitParams permit parameters: amount, deadline, v, s, r + * @param _accountsProof merkle proof for caller + * @param _codesProof merkle proof for code hash if caller is smart contract + */ + function userDepositVault( + address _beneficiary, + uint256 _userDepositUT, + bytes calldata _permitParams, + bytes32[] calldata _accountsProof, + bytes32[] calldata _codesProof + ) external returns (uint256); + + /** + * @dev the vault underlying token contract address + */ + function underlyingToken() external returns (address); +} + +contract MetaVault is ERC2771Context { + using SafeERC20 for IERC20; + + // solhint-disable-next-line no-empty-blocks + constructor(address forwarder) ERC2771Context(forwarder) {} + + /** + * @notice Deposit underlying tokens to the a given OptyFi vault + * @param _vault the address of the target vault, + * @param _userDepositUT Amount in underlying token + * @param _permitParams permit parameters: amount, deadline, v, s, r + * @param _accountsProof merkle proof for caller + * @param _codesProof merkle proof for code hash if caller is smart contract + */ + function deposit( + address _vault, + uint256 _userDepositUT, + bytes calldata _permitParams, + bytes32[] calldata _accountsProof, + bytes32[] calldata _codesProof + ) public returns (uint256) { + address _underlyingToken = IVault(_vault).underlyingToken(); + + _permit(_underlyingToken, _permitParams); + IERC20(_underlyingToken).safeTransferFrom(_msgSender(), address(this), _userDepositUT); + IERC20(_underlyingToken).approve(_vault, _userDepositUT); + + return IVault(_vault).userDepositVault(_msgSender(), _userDepositUT, "0x", _accountsProof, _codesProof); + } + + /* solhint-disable avoid-low-level-calls*/ + /** + * @dev execute the permit according to the permit param + * @param _permitParams data + */ + function _permit(address _token, bytes calldata _permitParams) internal { + if (_permitParams.length == 32 * 7) { + (bool success, ) = _token.call(abi.encodePacked(IERC20Permit.permit.selector, _permitParams)); + require(success, "!permit"); + } + + if (_permitParams.length == 32 * 8) { + (bool success, ) = _token.call(abi.encodePacked(IERC20PermitLegacy.permit.selector, _permitParams)); + require(success, "!legacy_permit"); + } + } +} From ee3bd438ce21136afafc29f7f4e6b0adee59a8e0 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Thu, 8 Sep 2022 22:32:49 -0400 Subject: [PATCH 04/30] feat(meta-vault-test): add meta vault test --- test/test-opty/meta-vault.spec.ts | 148 ++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 test/test-opty/meta-vault.spec.ts diff --git a/test/test-opty/meta-vault.spec.ts b/test/test-opty/meta-vault.spec.ts new file mode 100644 index 000000000..1559b4d24 --- /dev/null +++ b/test/test-opty/meta-vault.spec.ts @@ -0,0 +1,148 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; +import chai, { expect } from "chai"; +import { solidity } from "ethereum-waffle"; +import { BigNumber } from "ethers"; +import { deployments, ethers } from "hardhat"; +import { eEVMNetwork } from "../../helper-hardhat-config"; +import { MULTI_CHAIN_VAULT_TOKENS } from "../../helpers/constants/tokens"; +import { + getAccountsMerkleProof, + getAccountsMerkleRoot, + getCodesMerkleProof, + getCodesMerkleRoot, + Signers, + to_10powNumber_BN, +} from "../../helpers/utils"; +import { signMetaTxRequest } from "./utils"; +import { + ERC20Permit, + ERC20Permit__factory, + MetaVault, + MinimalForwarder, + Registry, + Registry__factory, + Vault, + Vault__factory, +} from "../../typechain"; +import { getPermitSignature, setTokenBalanceInStorage } from "./utils"; + +chai.use(solidity); +const fork = process.env.FORK as eEVMNetwork; + +async function deploy(name: string, ...params: any[]) { + const Contract = await ethers.getContractFactory(name); + return await Contract.deploy(...params).then(f => f.deployed()); +} + +describe("MetaVault", function () { + before(async function () { + //deploy contracts + await deployments.fixture(); + this.forwarder = await deploy("MinimalForwarder"); + this.metaVault = await deploy("MetaVault", this.forwarder.address); + + const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get("opUSDCgrow")).address; + const REGISTRY_PROXY_ADDRESS = (await deployments.get("RegistryProxy")).address; + + this.vault = await ethers.getContractAt(Vault__factory.abi, OPUSDCGROW_VAULT_ADDRESS); + this.registry = await ethers.getContractAt(Registry__factory.abi, REGISTRY_PROXY_ADDRESS); + + //Get signers + const financeOperatorAddress = await this.registry.getFinanceOperator(); + const governanceAddress = await this.registry.getGovernance(); + const signers: SignerWithAddress[] = await ethers.getSigners(); + this.signers = {} as Signers; + this.signers.financeOperator = await ethers.getSigner(financeOperatorAddress); + this.signers.governance = await ethers.getSigner(governanceAddress); + this.signers.alice = signers[0]; + this.relayer = signers[1]; + + //set vault config + const _vaultConfiguration = BigNumber.from( + "908337486804231642580332837833655270430560746049134246454386846501909299200", + ); + await this.vault.connect(this.signers.governance).setVaultConfiguration(_vaultConfiguration); + await this.vault.connect(this.signers.financeOperator).setValueControlParams( + "10000000000", // 10,000 USDC + "1000000000", // 1000 USDC + "1000000000000", // 1,000,000 USDC + ); + + //whitelist accounts and code + const code = await ethers.provider.getCode(this.metaVault.address); + const codeHash = ethers.utils.keccak256(code); + const _codeRoot = getCodesMerkleRoot([codeHash]); + await this.vault.connect(this.signers.governance).setWhitelistedCodesRoot(_codeRoot); + + const _accountsRoot = getAccountsMerkleRoot([this.signers.alice.address, this.metaVault.address]); + await this.vault.connect(this.signers.governance).setWhitelistedAccountsRoot(_accountsRoot); + + //add USDC balance to user + this.usdc = ( + await ethers.getContractAt(ERC20Permit__factory.abi, MULTI_CHAIN_VAULT_TOKENS[fork].USDC.address) + ); + await setTokenBalanceInStorage(this.usdc, this.signers.alice.address, "20000"); + + this.accountsProof = getAccountsMerkleProof( + [this.signers.alice.address, this.metaVault.address], + this.metaVault.address, + ); + this.codeProof = getCodesMerkleProof([codeHash], ethers.constants.HashZero); + + this.depositAmountUSDC = BigNumber.from("1000").mul(to_10powNumber_BN("6")); + }); + describe("MetaVault Test", function () { + beforeEach(async function () { + const deadline = ethers.constants.MaxUint256; + const sig = await getPermitSignature( + this.signers.alice, + this.usdc, + this.metaVault.address, + this.depositAmountUSDC, + deadline, + { version: "2" }, + ); + this.dataMetaVaultPermit = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"], + [this.signers.alice.address, this.metaVault.address, this.depositAmountUSDC, deadline, sig.v, sig.r, sig.s], + ); + }); + + it("deposit using MetaVaults directly", async function () { + await expect( + this.metaVault + .connect(this.signers.alice) + .deposit( + this.vault.address, + this.depositAmountUSDC, + this.dataMetaVaultPermit, + this.accountsProof, + this.codeProof, + ), + ) + .to.emit(this.vault, "Transfer") + .withArgs(ethers.constants.AddressZero, this.signers.alice.address, this.depositAmountUSDC); + }); + + it("deposits via a meta-tx", async function () { + const forwarder = this.forwarder.connect(this.relayer); + + const dataCall = this.metaVault.interface.encodeFunctionData("deposit", [ + this.vault.address, + this.depositAmountUSDC, + this.dataMetaVaultPermit, + this.accountsProof, + this.codeProof, + ]); + const { request, signature } = await signMetaTxRequest(this.signers.alice.provider, forwarder, { + from: this.signers.alice.address, + to: this.metaVault.address, + data: dataCall, + }); + + await expect(forwarder.execute(request, signature)) + .to.emit(this.vault, "Transfer") + .withArgs(ethers.constants.AddressZero, this.signers.alice.address, this.depositAmountUSDC); + }); + }); +}); From c8ef4fb728ecbbab268c6b771c8e267fa7b99dda Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Mon, 26 Sep 2022 16:11:02 -0400 Subject: [PATCH 05/30] fix(meta-vault): remove codes proof add expected output --- contracts/protocol/meta-tx/MetaVault.sol | 25 ++++++------------------ 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/contracts/protocol/meta-tx/MetaVault.sol b/contracts/protocol/meta-tx/MetaVault.sol index 406957f21..b1db06766 100644 --- a/contracts/protocol/meta-tx/MetaVault.sol +++ b/contracts/protocol/meta-tx/MetaVault.sol @@ -21,27 +21,14 @@ interface IERC20PermitLegacy { } interface IVault { - /** - * @notice Deposit underlying tokens to the vault - * @dev Mint the shares right away as per oracle based price per full share value - * @param _beneficiary the address of the deposit beneficiary, - * if _beneficiary = address(0) => _beneficiary = msg.sender - * @param _userDepositUT Amount in underlying token - * @param _permitParams permit parameters: amount, deadline, v, s, r - * @param _accountsProof merkle proof for caller - * @param _codesProof merkle proof for code hash if caller is smart contract - */ function userDepositVault( address _beneficiary, uint256 _userDepositUT, + uint256 _expectedOutput, bytes calldata _permitParams, - bytes32[] calldata _accountsProof, - bytes32[] calldata _codesProof + bytes32[] calldata _accountsProof ) external returns (uint256); - /** - * @dev the vault underlying token contract address - */ function underlyingToken() external returns (address); } @@ -55,16 +42,16 @@ contract MetaVault is ERC2771Context { * @notice Deposit underlying tokens to the a given OptyFi vault * @param _vault the address of the target vault, * @param _userDepositUT Amount in underlying token + * @param _expectedOutput Minimum amount of vault tokens minted after fees * @param _permitParams permit parameters: amount, deadline, v, s, r * @param _accountsProof merkle proof for caller - * @param _codesProof merkle proof for code hash if caller is smart contract */ function deposit( address _vault, uint256 _userDepositUT, + uint256 _expectedOutput, bytes calldata _permitParams, - bytes32[] calldata _accountsProof, - bytes32[] calldata _codesProof + bytes32[] calldata _accountsProof ) public returns (uint256) { address _underlyingToken = IVault(_vault).underlyingToken(); @@ -72,7 +59,7 @@ contract MetaVault is ERC2771Context { IERC20(_underlyingToken).safeTransferFrom(_msgSender(), address(this), _userDepositUT); IERC20(_underlyingToken).approve(_vault, _userDepositUT); - return IVault(_vault).userDepositVault(_msgSender(), _userDepositUT, "0x", _accountsProof, _codesProof); + return IVault(_vault).userDepositVault(_msgSender(), _userDepositUT, _expectedOutput, "0x", _accountsProof); } /* solhint-disable avoid-low-level-calls*/ From 24827495d8b888e10eb5be1ee3cccd578bc36dc0 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Mon, 26 Sep 2022 16:12:33 -0400 Subject: [PATCH 06/30] fix(test): refactor tests --- test/test-opty/meta-vault.spec.ts | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/test/test-opty/meta-vault.spec.ts b/test/test-opty/meta-vault.spec.ts index 1559b4d24..8bb96c60a 100644 --- a/test/test-opty/meta-vault.spec.ts +++ b/test/test-opty/meta-vault.spec.ts @@ -5,14 +5,7 @@ import { BigNumber } from "ethers"; import { deployments, ethers } from "hardhat"; import { eEVMNetwork } from "../../helper-hardhat-config"; import { MULTI_CHAIN_VAULT_TOKENS } from "../../helpers/constants/tokens"; -import { - getAccountsMerkleProof, - getAccountsMerkleRoot, - getCodesMerkleProof, - getCodesMerkleRoot, - Signers, - to_10powNumber_BN, -} from "../../helpers/utils"; +import { getAccountsMerkleProof, getAccountsMerkleRoot, Signers, to_10powNumber_BN } from "../../helpers/utils"; import { signMetaTxRequest } from "./utils"; import { ERC20Permit, @@ -41,7 +34,7 @@ describe("MetaVault", function () { this.forwarder = await deploy("MinimalForwarder"); this.metaVault = await deploy("MetaVault", this.forwarder.address); - const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get("opUSDCgrow")).address; + const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get("opUSDCearn")).address; const REGISTRY_PROXY_ADDRESS = (await deployments.get("RegistryProxy")).address; this.vault = await ethers.getContractAt(Vault__factory.abi, OPUSDCGROW_VAULT_ADDRESS); @@ -68,12 +61,6 @@ describe("MetaVault", function () { "1000000000000", // 1,000,000 USDC ); - //whitelist accounts and code - const code = await ethers.provider.getCode(this.metaVault.address); - const codeHash = ethers.utils.keccak256(code); - const _codeRoot = getCodesMerkleRoot([codeHash]); - await this.vault.connect(this.signers.governance).setWhitelistedCodesRoot(_codeRoot); - const _accountsRoot = getAccountsMerkleRoot([this.signers.alice.address, this.metaVault.address]); await this.vault.connect(this.signers.governance).setWhitelistedAccountsRoot(_accountsRoot); @@ -87,7 +74,6 @@ describe("MetaVault", function () { [this.signers.alice.address, this.metaVault.address], this.metaVault.address, ); - this.codeProof = getCodesMerkleProof([codeHash], ethers.constants.HashZero); this.depositAmountUSDC = BigNumber.from("1000").mul(to_10powNumber_BN("6")); }); @@ -115,9 +101,9 @@ describe("MetaVault", function () { .deposit( this.vault.address, this.depositAmountUSDC, + BigNumber.from("0"), this.dataMetaVaultPermit, this.accountsProof, - this.codeProof, ), ) .to.emit(this.vault, "Transfer") @@ -130,10 +116,11 @@ describe("MetaVault", function () { const dataCall = this.metaVault.interface.encodeFunctionData("deposit", [ this.vault.address, this.depositAmountUSDC, + BigNumber.from("0"), this.dataMetaVaultPermit, this.accountsProof, - this.codeProof, ]); + const { request, signature } = await signMetaTxRequest(this.signers.alice.provider, forwarder, { from: this.signers.alice.address, to: this.metaVault.address, From 5e88fd78d98ba567e764984a32e61cbb35d781d6 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Mon, 26 Sep 2022 16:27:10 -0400 Subject: [PATCH 07/30] feat(packages): add @opengsn/contracs --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b99fe8a14..34233d065 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ }, "dependencies": { "@chainlink/contracts": "^0.3.1", + "@opengsn/contracts": "^3.0.0-beta.1", "@openzeppelin/contracts": "3.4.0", "@openzeppelin/contracts-0.8.x": "npm:@openzeppelin/contracts@4.4.1", "@optyfi/defi-legos": "v0.1.0-rc.38", From dbe870a2253af8fbc5568f490e0eba1e3ed70347 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Tue, 27 Sep 2022 20:53:03 -0400 Subject: [PATCH 08/30] style(meta-vault): fix import style --- contracts/protocol/meta-tx/MetaVault.sol | 44 +++++++----------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/contracts/protocol/meta-tx/MetaVault.sol b/contracts/protocol/meta-tx/MetaVault.sol index b1db06766..81838fb45 100644 --- a/contracts/protocol/meta-tx/MetaVault.sol +++ b/contracts/protocol/meta-tx/MetaVault.sol @@ -1,37 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import "@openzeppelin/contracts-0.8.x/metatx/ERC2771Context.sol"; -import "@openzeppelin/contracts-0.8.x/metatx/MinimalForwarder.sol"; -import "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts-0.8.x/token/ERC20/extensions/draft-IERC20Permit.sol"; - -interface IERC20PermitLegacy { - function permit( - address holder, - address spender, - uint256 nonce, - uint256 expiry, - bool allowed, - uint8 v, - bytes32 r, - bytes32 s - ) external; -} - -interface IVault { - function userDepositVault( - address _beneficiary, - uint256 _userDepositUT, - uint256 _expectedOutput, - bytes calldata _permitParams, - bytes32[] calldata _accountsProof - ) external returns (uint256); - - function underlyingToken() external returns (address); -} - +import { ERC2771Context } from "@openzeppelin/contracts-0.8.x/metatx/ERC2771Context.sol"; +import { MinimalForwarder } from "@openzeppelin/contracts-0.8.x/metatx/MinimalForwarder.sol"; +import { SafeERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; +import { IVault } from "./interfaces/IVault.sol"; +import { IERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-0.8.x/token/ERC20/extensions/draft-IERC20Permit.sol"; +import { IERC20PermitLegacy } from "./interfaces/IERC20PermitLegacy.sol"; + +/** + * @title Vault contract inspired by AAVE V3's AToken.sol + * @author opty.fi + * @notice Implementation of the risk specific interest bearing vault + */ contract MetaVault is ERC2771Context { using SafeERC20 for IERC20; From 3f2f78e27a7e9601bc6dafd6a770ccbe2886fd8d Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Tue, 27 Sep 2022 20:54:50 -0400 Subject: [PATCH 09/30] feat(packages): add solidstate contracts --- .gitmodules | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitmodules b/.gitmodules index 6b7e0473c..883aac962 100644 --- a/.gitmodules +++ b/.gitmodules @@ -44,3 +44,6 @@ path = contracts/protocol/earn-protocol-configuration url = git@github.com:Opty-Fi/earn-protocol-configuration.git branch = code-size-fix +[submodule "contracts/protocol/optyfi-oracle"] + path = contracts/protocol/optyfi-oracle + url = https://github.com/Opty-Fi/optyfi-oracle.git From 8cf5660e1972d0c65892d24e1e2f7f46c46df4ce Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Tue, 27 Sep 2022 20:58:30 -0400 Subject: [PATCH 10/30] feat(packages): add solidstate contracts --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 34233d065..0859b98eb 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ "@openzeppelin/contracts": "3.4.0", "@openzeppelin/contracts-0.8.x": "npm:@openzeppelin/contracts@4.4.1", "@optyfi/defi-legos": "v0.1.0-rc.38", + "@solidstate/contracts": "^0.0.35", "@uniswap/v2-core": "^1.0.1", "@uniswap/v2-periphery": "^1.1.0-beta.0" } From 7d78071c4f82d168faf0f2e1d02275d376284775 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Tue, 27 Sep 2022 21:00:39 -0400 Subject: [PATCH 11/30] feat(opengsn): base paymaster and open gsn libraries and interfaces --- contracts/protocol/meta-tx/BasePaymaster.sol | 172 ++++++++++ .../meta-tx/interfaces/IERC20Token.sol | 18 + .../meta-tx/interfaces/IERC2771Recipient.sol | 35 ++ .../meta-tx/interfaces/IForwarder.sol | 95 ++++++ .../meta-tx/interfaces/IGsnRelayHub.sol | 23 ++ .../meta-tx/interfaces/IPaymaster.sol | 137 ++++++++ .../meta-tx/interfaces/IPenalizer.sol | 71 ++++ .../protocol/meta-tx/interfaces/IRelayHub.sol | 320 ++++++++++++++++++ .../meta-tx/interfaces/IRelayRegistrar.sol | 86 +++++ .../meta-tx/interfaces/IStakeManager.sol | 244 +++++++++++++ .../meta-tx/libraries/GsnEip712Library.sol | 161 +++++++++ .../protocol/meta-tx/libraries/GsnTypes.sol | 23 ++ .../protocol/meta-tx/libraries/GsnUtils.sol | 57 ++++ .../meta-tx/libraries/MinLibBytes.sol | 72 ++++ 14 files changed, 1514 insertions(+) create mode 100644 contracts/protocol/meta-tx/BasePaymaster.sol create mode 100644 contracts/protocol/meta-tx/interfaces/IERC20Token.sol create mode 100644 contracts/protocol/meta-tx/interfaces/IERC2771Recipient.sol create mode 100644 contracts/protocol/meta-tx/interfaces/IForwarder.sol create mode 100644 contracts/protocol/meta-tx/interfaces/IGsnRelayHub.sol create mode 100644 contracts/protocol/meta-tx/interfaces/IPaymaster.sol create mode 100644 contracts/protocol/meta-tx/interfaces/IPenalizer.sol create mode 100644 contracts/protocol/meta-tx/interfaces/IRelayHub.sol create mode 100644 contracts/protocol/meta-tx/interfaces/IRelayRegistrar.sol create mode 100644 contracts/protocol/meta-tx/interfaces/IStakeManager.sol create mode 100644 contracts/protocol/meta-tx/libraries/GsnEip712Library.sol create mode 100644 contracts/protocol/meta-tx/libraries/GsnTypes.sol create mode 100644 contracts/protocol/meta-tx/libraries/GsnUtils.sol create mode 100644 contracts/protocol/meta-tx/libraries/MinLibBytes.sol diff --git a/contracts/protocol/meta-tx/BasePaymaster.sol b/contracts/protocol/meta-tx/BasePaymaster.sol new file mode 100644 index 000000000..228bc17bf --- /dev/null +++ b/contracts/protocol/meta-tx/BasePaymaster.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165Checker.sol"; + +import "./libraries/GsnTypes.sol"; +import "./interfaces/IPaymaster.sol"; +import "./interfaces/IRelayHub.sol"; +import "./libraries/GsnEip712Library.sol"; +import "./interfaces/IForwarder.sol"; + +/** + * @notice An abstract base class to be inherited by a concrete Paymaster. + * A subclass must implement: + * - preRelayedCall + * - postRelayedCall + */ +abstract contract BasePaymaster is IPaymaster, Ownable, ERC165 { + using ERC165Checker for address; + + IRelayHub internal relayHub; + address private _trustedForwarder; + + /// @inheritdoc IPaymaster + function getRelayHub() public view override returns (address) { + return address(relayHub); + } + + //overhead of forwarder verify+signature, plus hub overhead. + uint256 public constant FORWARDER_HUB_OVERHEAD = 50000; + + //These parameters are documented in IPaymaster.GasAndDataLimits + uint256 public constant PRE_RELAYED_CALL_GAS_LIMIT = 100000; + uint256 public constant POST_RELAYED_CALL_GAS_LIMIT = 110000; + uint256 public constant PAYMASTER_ACCEPTANCE_BUDGET = PRE_RELAYED_CALL_GAS_LIMIT + FORWARDER_HUB_OVERHEAD; + uint256 public constant CALLDATA_SIZE_LIMIT = 10500; + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return + interfaceId == type(IPaymaster).interfaceId || + interfaceId == type(Ownable).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @inheritdoc IPaymaster + function getGasAndDataLimits() public view virtual override returns (IPaymaster.GasAndDataLimits memory limits) { + return + IPaymaster.GasAndDataLimits( + PAYMASTER_ACCEPTANCE_BUDGET, + PRE_RELAYED_CALL_GAS_LIMIT, + POST_RELAYED_CALL_GAS_LIMIT, + CALLDATA_SIZE_LIMIT + ); + } + + /** + * @notice this method must be called from preRelayedCall to validate that the forwarder + * is approved by the paymaster as well as by the recipient contract. + */ + function _verifyForwarder(GsnTypes.RelayRequest calldata relayRequest) internal view virtual { + require(getTrustedForwarder() == relayRequest.relayData.forwarder, "Forwarder is not trusted"); + GsnEip712Library.verifyForwarderTrusted(relayRequest); + } + + function _verifyRelayHubOnly() internal view virtual { + require(msg.sender == getRelayHub(), "can only be called by RelayHub"); + } + + function _verifyValue(GsnTypes.RelayRequest calldata relayRequest) internal view virtual { + require(relayRequest.request.value == 0, "value transfer not supported"); + } + + function _verifyPaymasterData(GsnTypes.RelayRequest calldata relayRequest) internal view virtual { + require(relayRequest.relayData.paymasterData.length == 0, "should have no paymasterData"); + } + + function _verifyApprovalData(bytes calldata approvalData) internal view virtual { + require(approvalData.length == 0, "should have no approvalData"); + } + + /** + * @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. + */ + function setRelayHub(IRelayHub hub) public onlyOwner { + require(address(hub).supportsInterface(type(IRelayHub).interfaceId), "target is not a valid 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. + */ + function setTrustedForwarder(address forwarder) public virtual onlyOwner { + require(forwarder.supportsInterface(type(IForwarder).interfaceId), "target is not a valid IForwarder"); + _trustedForwarder = forwarder; + } + + function getTrustedForwarder() public view virtual override returns (address) { + return _trustedForwarder; + } + + /** + * @notice Any native Ether 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 virtual { + require(address(relayHub) != address(0), "relay hub address not set"); + 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. + */ + function withdrawRelayHubDepositTo(uint256 amount, address payable target) public onlyOwner { + relayHub.withdraw(target, amount); + } + + /// @inheritdoc IPaymaster + function preRelayedCall( + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData, + uint256 maxPossibleGas + ) external override returns (bytes memory, bool) { + _verifyRelayHubOnly(); + _verifyForwarder(relayRequest); + _verifyValue(relayRequest); + _verifyPaymasterData(relayRequest); + _verifyApprovalData(approvalData); + return _preRelayedCall(relayRequest, signature, approvalData, maxPossibleGas); + } + + /** + * @notice internal logic the paymasters need to provide to select which transactions they are willing to pay for + * @notice see the documentation for `IPaymaster::preRelayedCall` for details + */ + function _preRelayedCall( + GsnTypes.RelayRequest calldata, + bytes calldata, + bytes calldata, + uint256 + ) internal virtual returns (bytes memory, bool); + + /// @inheritdoc IPaymaster + function postRelayedCall( + bytes calldata context, + bool success, + uint256 gasUseWithoutPost, + GsnTypes.RelayData calldata relayData + ) external override { + _verifyRelayHubOnly(); + _postRelayedCall(context, success, gasUseWithoutPost, relayData); + } + + /** + * @notice internal logic the paymasters need to provide if they need to take some action after the transaction + * @notice see the documentation for `IPaymaster::postRelayedCall` for details + */ + function _postRelayedCall( + bytes calldata, + bool, + uint256, + GsnTypes.RelayData calldata + ) internal virtual; +} diff --git a/contracts/protocol/meta-tx/interfaces/IERC20Token.sol b/contracts/protocol/meta-tx/interfaces/IERC20Token.sol new file mode 100644 index 000000000..76b5913fe --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IERC20Token.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC20/IERC20.sol) + +pragma solidity >=0.7.6; + +import "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts-0.8.x/token/ERC20/extensions/IERC20Metadata.sol"; + +/** + * @notice Extended ERC-20 token interface used internally in OpenGSN modules. + * Renamed to avoid conflict with OZ namespace. Includes IERC20, ERC20Metadata. + * added semi-standard "wrapped eth" access methods deposit() and "withdraw()" + */ +interface IERC20Token is IERC20, IERC20Metadata { + function deposit() external payable; + + function withdraw(uint256 amount) external; +} diff --git a/contracts/protocol/meta-tx/interfaces/IERC2771Recipient.sol b/contracts/protocol/meta-tx/interfaces/IERC2771Recipient.sol new file mode 100644 index 000000000..c037cc481 --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IERC2771Recipient.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.0; + +/** + * @title The ERC-2771 Recipient Base Abstract Class - Declarations + * + * @notice A contract must implement this interface in order to support relayed transaction. + * + * @notice It is recommended that your contract inherits from the ERC2771Recipient contract. + */ +abstract contract IERC2771Recipient { + /** + * :warning: **Warning** :warning: The Forwarder can have a full control over your Recipient. Only trust verified Forwarder. + * @param forwarder The address of the Forwarder contract that is being used. + * @return isTrustedForwarder `true` if the Forwarder is trusted to forward relayed transactions by this Recipient. + */ + function isTrustedForwarder(address forwarder) public view virtual returns (bool); + + /** + * @notice Use this method the contract anywhere instead of msg.sender to support relayed transactions. + * @return sender The real sender of this call. + * For a call that came through the Forwarder the real sender is extracted from the last 20 bytes of the `msg.data`. + * Otherwise simply returns `msg.sender`. + */ + function _msgSender() internal view virtual returns (address); + + /** + * @notice Use this method in the contract instead of `msg.data` when difference matters (hashing, signature, etc.) + * @return data The real `msg.data` of this call. + * For a call that came through the Forwarder, the real sender address was appended as the last 20 bytes + * of the `msg.data` - so this method will strip those 20 bytes off. + * Otherwise (if the call was made directly and not through the forwarder) simply returns `msg.data`. + */ + function _msgData() internal view virtual returns (bytes calldata); +} diff --git a/contracts/protocol/meta-tx/interfaces/IForwarder.sol b/contracts/protocol/meta-tx/interfaces/IForwarder.sol new file mode 100644 index 000000000..1cec711ef --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IForwarder.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/interfaces/IERC165.sol"; + +interface IForwarder is IERC165 { + /** + * @notice A representation of a request for a `Forwarder` to send `data` on behalf of a `from` to a target (`to`). + */ + struct ForwardRequest { + address from; + address to; + uint256 value; + uint256 gas; + uint256 nonce; + bytes data; + uint256 validUntilTime; + } + + event DomainRegistered(bytes32 indexed domainSeparator, bytes domainValue); + + event RequestTypeRegistered(bytes32 indexed typeHash, string typeStr); + + /** + * @param from The address of a sender. + * @return The nonce for this address. + */ + function getNonce(address from) external view returns (uint256); + + /** + * @notice Verify the transaction is valid and can be executed. + * Implementations must validate the signature and the nonce of the request are correct. + * Does not revert and returns successfully if the input is valid. + * Reverts if any validation has failed. For instance, if either signature or nonce are incorrect. + * Reverts if `domainSeparator` or `requestTypeHash` are not registered as well. + */ + function verify( + ForwardRequest calldata forwardRequest, + bytes32 domainSeparator, + bytes32 requestTypeHash, + bytes calldata suffixData, + bytes calldata signature + ) external view; + + /** + * @notice Executes a transaction specified by the `ForwardRequest`. + * The transaction is first verified and then executed. + * The success flag and returned bytes array of the `CALL` are returned as-is. + * + * This method would revert only in case of a verification error. + * + * All the target errors are reported using the returned success flag and returned bytes array. + * + * @param forwardRequest All requested transaction parameters. + * @param domainSeparator The domain used when signing this request. + * @param requestTypeHash The request type used when signing this request. + * @param suffixData The ABI-encoded extension data for the current `RequestType` used when signing this request. + * @param signature The client signature to be validated. + * + * @return success The success flag of the underlying `CALL` to the target address. + * @return ret The byte array returned by the underlying `CALL` to the target address. + */ + function execute( + ForwardRequest calldata forwardRequest, + bytes32 domainSeparator, + bytes32 requestTypeHash, + bytes calldata suffixData, + bytes calldata signature + ) external payable returns (bool success, bytes memory ret); + + /** + * @notice Register a new Request typehash. + * + * @notice This is necessary for the Forwarder to be able to verify the signatures conforming to the ERC-712. + * + * @param typeName The name of the request type. + * @param typeSuffix Any extra data after the generic params. Must contain add at least one param. + * The generic ForwardRequest type is always registered by the constructor. + */ + function registerRequestType(string calldata typeName, string calldata typeSuffix) external; + + /** + * @notice Register a new domain separator. + * + * @notice This is necessary for the Forwarder to be able to verify the signatures conforming to the ERC-712. + * + * @notice The domain separator must have the following fields: `name`, `version`, `chainId`, `verifyingContract`. + * The `chainId` is the current network's `chainId`, and the `verifyingContract` is this Forwarder's address. + * This method accepts the domain name and version to create and register the domain separator value. + * @param name The domain's display name. + * @param version The domain/protocol version. + */ + function registerDomainSeparator(string calldata name, string calldata version) external; +} diff --git a/contracts/protocol/meta-tx/interfaces/IGsnRelayHub.sol b/contracts/protocol/meta-tx/interfaces/IGsnRelayHub.sol new file mode 100644 index 000000000..9686f227f --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IGsnRelayHub.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier:MIT + +pragma solidity ^0.8.0; + +import "../libraries/GsnTypes.sol"; + +interface IGsnRelayHub { + function balanceOf(address target) external view returns (uint256); + + function calculateCharge(uint256 gasUsed, GsnTypes.RelayData calldata relayData) external view returns (uint256); + + function depositFor(address target) external payable; + + function relayCall( + uint256 maxAcceptanceBudget, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData, + uint256 externalGasLimit + ) external returns (bool paymasterAccepted, bytes memory returnValue); + + function withdraw(uint256 amount, address payable dest) external; +} diff --git a/contracts/protocol/meta-tx/interfaces/IPaymaster.sol b/contracts/protocol/meta-tx/interfaces/IPaymaster.sol new file mode 100644 index 000000000..9758365a2 --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IPaymaster.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/interfaces/IERC165.sol"; + +import "../libraries/GsnTypes.sol"; + +/** + * @title The Paymaster Interface + * @notice Contracts implementing this interface exist to make decision about paying the transaction fee to the relay. + * + * @notice There are two callbacks here that are executed by the RelayHub: `preRelayedCall` and `postRelayedCall`. + * + * @notice It is recommended that your implementation inherits from the abstract BasePaymaster contract. + */ +interface IPaymaster is IERC165 { + /** + * @notice The limits this Paymaster wants to be imposed by the RelayHub on user input. See `getGasAndDataLimits`. + */ + struct GasAndDataLimits { + uint256 acceptanceBudget; + uint256 preRelayedCallGasLimit; + uint256 postRelayedCallGasLimit; + uint256 calldataSizeLimit; + } + + /** + * @notice Return the Gas Limits for Paymaster's functions and maximum msg.data length values for this Paymaster. + * This function allows different paymasters to have different properties without changes to the RelayHub. + * @return limits An instance of the `GasAndDataLimits` struct + * + * ##### `acceptanceBudget` + * If the transactions consumes more than `acceptanceBudget` this Paymaster will be charged for gas no matter what. + * Transaction that gets rejected after consuming more than `acceptanceBudget` gas is on this Paymaster's expense. + * + * Should be set to an amount gas this Paymaster expects to spend deciding whether to accept or reject a request. + * This includes gas consumed by calculations in the `preRelayedCall`, `Forwarder` and the recipient contract. + * + * :warning: **Warning** :warning: As long this value is above `preRelayedCallGasLimit` + * (see defaults in `BasePaymaster`), the Paymaster is guaranteed it will never pay for rejected transactions. + * If this value is below `preRelayedCallGasLimit`, it might might make Paymaster open to a "griefing" attack. + * + * The relayers should prefer lower `acceptanceBudget`, as it improves their chances of being compensated. + * From a Relay's point of view, this is the highest gas value a bad Paymaster may cost the relay, + * since the paymaster will pay anything above that value regardless of whether the transaction succeeds or reverts. + * Specifying value too high might make the call rejected by relayers (see `maxAcceptanceBudget` in server config). + * + * ##### `preRelayedCallGasLimit` + * The max gas usage of preRelayedCall. Any revert of the `preRelayedCall` is a request rejection by the paymaster. + * As long as `acceptanceBudget` is above `preRelayedCallGasLimit`, any such revert is not payed by the paymaster. + * + * ##### `postRelayedCallGasLimit` + * The max gas usage of postRelayedCall. The Paymaster is not charged for the maximum, only for actually used gas. + * Note that an OOG will revert the inner transaction, but the paymaster will be charged for it anyway. + */ + function getGasAndDataLimits() external view returns (GasAndDataLimits memory limits); + + /** + * @notice :warning: **Warning** :warning: using incorrect Forwarder may cause the Paymaster to agreeing to pay for invalid transactions. + * @return trustedForwarder The address of the `Forwarder` that is trusted by this Paymaster to execute the requests. + */ + function getTrustedForwarder() external view returns (address trustedForwarder); + + /** + * @return relayHub The address of the `RelayHub` that is trusted by this Paymaster to execute the requests. + */ + function getRelayHub() external view returns (address relayHub); + + /** + * @notice Called by the Relay in view mode and later by the `RelayHub` on-chain to validate that + * the Paymaster agrees to pay for this call. + * + * The request is considered to be rejected by the Paymaster in one of the following conditions: + * - `preRelayedCall()` method reverts + * - the `Forwarder` reverts because of nonce or signature error + * - the `Paymaster` returned `rejectOnRecipientRevert: true` and the recipient contract reverted + * (and all that did not consume more than `acceptanceBudget` gas). + * + * In any of the above cases, all Paymaster calls and the recipient call are reverted. + * In any other case the Paymaster will pay for the gas cost of the transaction. + * Note that even if `postRelayedCall` is reverted the Paymaster will be charged. + * + + * @param relayRequest - the full relay request structure + * @param signature - user's EIP712-compatible signature of the `relayRequest`. + * Note that in most cases the paymaster shouldn't try use it at all. It is always checked + * by the forwarder immediately after preRelayedCall returns. + * @param approvalData - extra dapp-specific data (e.g. signature from trusted party) + * @param maxPossibleGas - based on values returned from `getGasAndDataLimits` + * the RelayHub will calculate the maximum possible amount of gas the user may be charged for. + * In order to convert this value to wei, the Paymaster has to call "relayHub.calculateCharge()" + * + * @return context + * A byte array to be passed to postRelayedCall. + * Can contain any data needed by this Paymaster in any form or be empty if no extra data is needed. + * @return rejectOnRecipientRevert + * The flag that allows a Paymaster to "delegate" the rejection to the recipient code. + * It also means the Paymaster trust the recipient to reject fast: both preRelayedCall, + * forwarder check and recipient checks must fit into the GasLimits.acceptanceBudget, + * otherwise the TX is paid by the Paymaster. + * `true` if the Paymaster wants to reject the TX if the recipient reverts. + * `false` if the Paymaster wants rejects by the recipient to be completed on chain and paid by the Paymaster. + */ + function preRelayedCall( + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData, + uint256 maxPossibleGas + ) external returns (bytes memory context, bool rejectOnRecipientRevert); + + /** + * @notice This method is called after the actual relayed function call. + * It may be used to record the transaction (e.g. charge the caller by some contract logic) for this call. + * + * Revert in this functions causes a revert of the client's relayed call (and preRelayedCall(), but the Paymaster + * is still committed to pay the relay for the entire transaction. + * + * @param context The call context, as returned by the preRelayedCall + * @param success `true` if the relayed call succeeded, false if it reverted + * @param gasUseWithoutPost The actual amount of gas used by the entire transaction, EXCEPT + * the gas used by the postRelayedCall itself. + * @param relayData The relay params of the request. can be used by relayHub.calculateCharge() + * + */ + function postRelayedCall( + bytes calldata context, + bool success, + uint256 gasUseWithoutPost, + GsnTypes.RelayData calldata relayData + ) external; + + /** + * @return version The SemVer string of this Paymaster's version. + */ + function versionPaymaster() external view returns (string memory); +} diff --git a/contracts/protocol/meta-tx/interfaces/IPenalizer.sol b/contracts/protocol/meta-tx/interfaces/IPenalizer.sol new file mode 100644 index 000000000..204715e2e --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IPenalizer.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; + +import "./IRelayHub.sol"; + +/** + * @title The Penalizer Interface + * @notice In some cases the behavior of a Relay Server may be found to be illegal. + * It is the responsibility of a `Penalizer` contract to judge whether there was a penalizable event. + * + * @notice In case there was, the `Penalizer` will direct the `RelayHub` to slash the stake of the faulty Relay Server. + */ +interface IPenalizer is IERC165 { + /// @notice Emitted once the reporter submits the first step in the commit-reveal process. + event CommitAdded(address indexed sender, bytes32 indexed commitHash, uint256 readyBlockNumber); + + struct Transaction { + uint256 nonce; + uint256 gasLimit; + address to; + uint256 value; + bytes data; + } + + /** + * @notice Called by the reporter as the first step in the commit-reveal process. + * Any sender can call it to make sure no-one can front-run it to claim this penalization. + * @param commitHash The hash of the report of a penalizable behaviour the reporter wants to reveal. + * Calculated as `commit(keccak(encodedPenalizeFunction))`. + */ + function commit(bytes32 commitHash) external; + + /** + * @notice Called by the reporter as the second step in the commit-reveal process. + * If a Relay Worker attacked the system by signing multiple transactions with same nonce so only one is accepted, + * anyone can grab both transactions from the blockchain and submit them here. + * Check whether `unsignedTx1` != `unsignedTx2`, that both are signed by the same address, + * and that `unsignedTx1.nonce` == `unsignedTx2.nonce`. + * If all conditions are met, relay is considered an "offending relay". + * The offending relay will be unregistered immediately, its stake will be forfeited and given + * to the address who reported it (the `msg.sender`), thus incentivizing anyone to report offending relays. + */ + function penalizeRepeatedNonce( + bytes calldata unsignedTx1, + bytes calldata signature1, + bytes calldata unsignedTx2, + bytes calldata signature2, + IRelayHub hub, + uint256 randomValue + ) external; + + /** + * @notice Called by the reporter as the second step in the commit-reveal process. + * The Relay Workers are not allowed to make calls other than to the `relayCall` method. + */ + function penalizeIllegalTransaction( + bytes calldata unsignedTx, + bytes calldata signature, + IRelayHub hub, + uint256 randomValue + ) external; + + /// @return a SemVer-compliant version of the `Penalizer` contract. + function versionPenalizer() external view returns (string memory); + + /// @return The minimum delay between commit and reveal steps. + function getPenalizeBlockDelay() external view returns (uint256); + + /// @return The maximum delay between commit and reveal steps. + function getPenalizeBlockExpiration() external view returns (uint256); +} diff --git a/contracts/protocol/meta-tx/interfaces/IRelayHub.sol b/contracts/protocol/meta-tx/interfaces/IRelayHub.sol new file mode 100644 index 000000000..5f72bf58d --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IRelayHub.sol @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/interfaces/IERC165.sol"; + +import "../libraries/GsnTypes.sol"; +import "./IStakeManager.sol"; + +/** + * @title The RelayHub interface + * @notice The implementation of this interface provides all the information the GSN client needs to + * create a valid `RelayRequest` and also serves as an entry point for such requests. + * + * @notice The RelayHub also handles all the related financial records and hold the balances of participants. + * The Paymasters keep their Ether deposited in the `RelayHub` in order to pay for the `RelayRequest`s that thay choose + * to pay for, and Relay Servers keep their earned Ether in the `RelayHub` until they choose to `withdraw()` + * + * @notice The RelayHub on each supported network only needs a single instance and there is usually no need for dApp + * developers or Relay Server operators to redeploy, reimplement, modify or override the `RelayHub`. + */ +interface IRelayHub is IERC165 { + /** + * @notice A struct that contains all the parameters of the `RelayHub` that can be modified after the deployment. + */ + struct RelayHubConfig { + // maximum number of worker accounts allowed per manager + uint256 maxWorkerCount; + // Gas set aside for all relayCall() instructions to prevent unexpected out-of-gas exceptions + uint256 gasReserve; + // Gas overhead to calculate gasUseWithoutPost + uint256 postOverhead; + // Gas cost of all relayCall() instructions after actual 'calculateCharge()' + // Assume that relay has non-zero balance (costs 15'000 more otherwise). + uint256 gasOverhead; + // Minimum unstake delay seconds of a relay manager's stake on the StakeManager + uint256 minimumUnstakeDelay; + // Developers address + address devAddress; + // 0 < fee < 100, as percentage of total charge from paymaster to relayer + uint8 devFee; + // baseRelayFee The base fee the Relay Server charges for a single transaction in Ether, in wei. + uint80 baseRelayFee; + // pctRelayFee The percent of the total charge to add as a Relay Server fee to the total charge. + uint16 pctRelayFee; + } + + /// @notice Emitted when a configuration of the `RelayHub` is changed + event RelayHubConfigured(RelayHubConfig config); + + /// @notice Emitted when relays are added by a relayManager + event RelayWorkersAdded(address indexed relayManager, address[] newRelayWorkers, uint256 workersCount); + + /// @notice Emitted when an account withdraws funds from the `RelayHub`. + event Withdrawn(address indexed account, address indexed dest, uint256 amount); + + /// @notice Emitted when `depositFor` is called, including the amount and account that was funded. + event Deposited(address indexed paymaster, address indexed from, uint256 amount); + + /// @notice Emitted for each token configured for staking in setMinimumStakes + event StakingTokenDataChanged(address token, uint256 minimumStake); + + /** + * @notice Emitted when an attempt to relay a call fails and the `Paymaster` does not accept the transaction. + * The actual relayed call was not executed, and the recipient not charged. + * @param reason contains a revert reason returned from preRelayedCall or forwarder. + */ + event TransactionRejectedByPaymaster( + address indexed relayManager, + address indexed paymaster, + bytes32 indexed relayRequestID, + address from, + address to, + address relayWorker, + bytes4 selector, + uint256 innerGasUsed, + bytes reason + ); + + /** + * @notice Emitted when a transaction is relayed. Note that the actual internal function call might be reverted. + * The reason for a revert will be indicated in the `status` field of a corresponding `RelayCallStatus` value. + * @notice `charge` is the Ether value deducted from the `Paymaster` balance. + * The amount added to the `relayManager` balance will be lower if there is an activated `devFee` in the `config`. + */ + event TransactionRelayed( + address indexed relayManager, + address indexed relayWorker, + bytes32 indexed relayRequestID, + address from, + address to, + address paymaster, + bytes4 selector, + RelayCallStatus status, + uint256 charge + ); + + /// @notice This event is emitted in case the internal function returns a value or reverts with a revert string. + event TransactionResult(RelayCallStatus status, bytes returnValue); + + /// @notice This event is emitted in case this `RelayHub` is deprecated and will stop serving transactions soon. + event HubDeprecated(uint256 deprecationTime); + + /** + * @notice This event is emitted in case a `relayManager` has been deemed "abandoned" for being + * unresponsive for a prolonged period of time. + * @notice This event means the entire balance of the relay has been transferred to the `devAddress`. + */ + event AbandonedRelayManagerBalanceEscheated(address indexed relayManager, uint256 balance); + + /** + * Error codes that describe all possible failure reasons reported in the `TransactionRelayed` event `status` field. + * @param OK The transaction was successfully relayed and execution successful - never included in the event. + * @param RelayedCallFailed The transaction was relayed, but the relayed call failed. + * @param RejectedByPreRelayed The transaction was not relayed due to preRelatedCall reverting. + * @param RejectedByForwarder The transaction was not relayed due to forwarder check (signature,nonce). + * @param PostRelayedFailed The transaction was relayed and reverted due to postRelatedCall reverting. + * @param PaymasterBalanceChanged The transaction was relayed and reverted due to the paymaster balance change. + */ + enum RelayCallStatus { + OK, + RelayedCallFailed, + RejectedByPreRelayed, + RejectedByForwarder, + RejectedByRecipientRevert, + PostRelayedFailed, + PaymasterBalanceChanged + } + + /** + * @notice Add new worker addresses controlled by the sender who must be a staked Relay Manager address. + * Emits a `RelayWorkersAdded` event. + * This function can be called multiple times, emitting new events. + */ + function addRelayWorkers(address[] calldata newRelayWorkers) external; + + /** + * @notice The `RelayRegistrar` callback to notify the `RelayHub` that this `relayManager` has updated registration. + */ + function onRelayServerRegistered(address relayManager) external; + + // Balance management + + /** + * @notice Deposits ether for a `Paymaster`, so that it can and pay for relayed transactions. + * :warning: **Warning** :warning: Unused balance can only be withdrawn by the holder itself, by calling `withdraw`. + * Emits a `Deposited` event. + */ + function depositFor(address target) external payable; + + /** + * @notice Withdraws from an account's balance, sending it back to the caller. + * Relay Managers call this to retrieve their revenue, and `Paymasters` can also use it to reduce their funding. + * Emits a `Withdrawn` event. + */ + function withdraw(address payable dest, uint256 amount) external; + + /** + * @notice Withdraws from an account's balance, sending funds to multiple provided addresses. + * Relay Managers call this to retrieve their revenue, and `Paymasters` can also use it to reduce their funding. + * Emits a `Withdrawn` event for each destination. + */ + function withdrawMultiple(address payable[] memory dest, uint256[] memory amount) external; + + // Relaying + + /** + * @notice Relays a transaction. For this to succeed, multiple conditions must be met: + * - `Paymaster`'s `preRelayCall` method must succeed and not revert. + * - the `msg.sender` must be a registered Relay Worker that the user signed to use. + * - the transaction's gas fees must be equal or larger than the ones that were signed by the sender. + * - the transaction must have enough gas to run all internal transactions if they use all gas available to them. + * - the `Paymaster` must have enough balance to pay the Relay Worker if all gas is spent. + * + * @notice If all conditions are met, the call will be relayed and the `Paymaster` charged. + * + * @param domainSeparatorName The name of the Domain Separator used to verify the EIP-712 signature + * @param maxAcceptanceBudget The maximum valid value for `paymaster.getGasLimits().acceptanceBudget` to return. + * @param relayRequest All details of the requested relayed call. + * @param signature The client's EIP-712 signature over the `relayRequest` struct. + * @param approvalData The dapp-specific data forwarded to the `Paymaster`'s `preRelayedCall` method. + * This value is **not** verified by the `RelayHub` in any way. + * As an example, it can be used to pass some kind of a third-party signature to the `Paymaster` for verification. + * + * Emits a `TransactionRelayed` event regardless of whether the transaction succeeded or failed. + */ + function relayCall( + string calldata domainSeparatorName, + uint256 maxAcceptanceBudget, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData + ) + external + returns ( + bool paymasterAccepted, + uint256 charge, + IRelayHub.RelayCallStatus status, + bytes memory returnValue + ); + + /** + * @notice In case the Relay Worker has been found to be in violation of some rules by the `Penalizer` contract, + * the `Penalizer` will call this method to execute a penalization. + * The `RelayHub` will look up the Relay Manager of the given Relay Worker and will forward the call to + * the `StakeManager` contract. The `RelayHub` does not perform the actual penalization either. + * @param relayWorker The address of the Relay Worker that committed a penalizable offense. + * @param beneficiary The address that called the `Penalizer` and will receive a reward for it. + */ + function penalize(address relayWorker, address payable beneficiary) external; + + /** + * @notice Sets or changes the configuration of this `RelayHub`. + * @param _config The new configuration. + */ + function setConfiguration(RelayHubConfig memory _config) external; + + /** + * @notice Sets or changes the minimum amount of a given `token` that needs to be staked so that the Relay Manager + * is considered to be 'staked' by this `RelayHub`. Zero value means this token is not allowed for staking. + * @param token An array of addresses of ERC-20 compatible tokens. + * @param minimumStake An array of minimal amounts necessary for a corresponding token, in wei. + */ + function setMinimumStakes(IERC20[] memory token, uint256[] memory minimumStake) external; + + /** + * @notice Deprecate hub by reverting all incoming `relayCall()` calls starting from a given timestamp + * @param _deprecationTime The timestamp in seconds after which the `RelayHub` stops serving transactions. + */ + function deprecateHub(uint256 _deprecationTime) external; + + /** + * @notice + * @param relayManager + */ + function escheatAbandonedRelayBalance(address relayManager) external; + + /** + * @notice The fee is expressed as a base fee in wei plus percentage of the actual charge. + * For example, a value '40' stands for a 40% fee, so the recipient will be charged for 1.4 times the spent amount. + * @param gasUsed An amount of gas used by the transaction. + * @param relayData The details of a transaction signed by the sender. + * @return The calculated charge, in wei. + */ + function calculateCharge(uint256 gasUsed, GsnTypes.RelayData calldata relayData) external view returns (uint256); + + /** + * @notice The fee is expressed as a percentage of the actual charge. + * For example, a value '40' stands for a 40% fee, so the Relay Manager will only get 60% of the `charge`. + * @param charge The amount of Ether in wei the Paymaster will be charged for this transaction. + * @return The calculated devFee, in wei. + */ + function calculateDevCharge(uint256 charge) external view returns (uint256); + + /* getters */ + + /// @return config The configuration of the `RelayHub`. + function getConfiguration() external view returns (RelayHubConfig memory config); + + /** + * @param token An address of an ERC-20 compatible tokens. + * @return The minimum amount of a given `token` that needs to be staked so that the Relay Manager + * is considered to be 'staked' by this `RelayHub`. Zero value means this token is not allowed for staking. + */ + function getMinimumStakePerToken(IERC20 token) external view returns (uint256); + + /** + * @param worker An address of the Relay Worker. + * @return The address of its Relay Manager. + */ + function getWorkerManager(address worker) external view returns (address); + + /** + * @param manager An address of the Relay Manager. + * @return The count of Relay Workers associated with this Relay Manager. + */ + function getWorkerCount(address manager) external view returns (uint256); + + /// @return An account's balance. It can be either a deposit of a `Paymaster`, or a revenue of a Relay Manager. + function balanceOf(address target) external view returns (uint256); + + /// @return The `StakeManager` address for this `RelayHub`. + function getStakeManager() external view returns (IStakeManager); + + /// @return The `Penalizer` address for this `RelayHub`. + function getPenalizer() external view returns (address); + + /// @return The `RelayRegistrar` address for this `RelayHub`. + function getRelayRegistrar() external view returns (address); + + /// @return The `BatchGateway` address for this `RelayHub`. + function getBatchGateway() external view returns (address); + + /** + * @notice Uses `StakeManager` to decide if the Relay Manager can be considered staked or not. + * Returns if the stake's token, amount and delay satisfy all requirements, reverts otherwise. + */ + function verifyRelayManagerStaked(address relayManager) external view; + + /** + * @notice Uses `StakeManager` to check if the Relay Manager can be considered abandoned or not. + * Returns true if the stake's abandonment time is in the past including the escheatment delay, false otherwise. + */ + function isRelayEscheatable(address relayManager) external view returns (bool); + + /// @return `true` if the `RelayHub` is deprecated, `false` it it is not deprecated and can serve transactions. + function isDeprecated() external view returns (bool); + + /// @return The timestamp from which the hub no longer allows relaying calls. + function getDeprecationTime() external view returns (uint256); + + /// @return The block number in which the contract has been deployed. + function getCreationBlock() external view returns (uint256); + + /// @return a SemVer-compliant version of the `RelayHub` contract. + function versionHub() external view returns (string memory); + + /// @return A total measurable amount of gas left to current execution. Same as 'gasleft()' for pure EVMs. + function aggregateGasleft() external view returns (uint256); +} diff --git a/contracts/protocol/meta-tx/interfaces/IRelayRegistrar.sol b/contracts/protocol/meta-tx/interfaces/IRelayRegistrar.sol new file mode 100644 index 000000000..b608ee6c3 --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IRelayRegistrar.sol @@ -0,0 +1,86 @@ +//SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.6; + +import "@openzeppelin/contracts-0.8.x/interfaces/IERC165.sol"; + +/** + * @title The RelayRegistrar Interface + * @notice The on-chain registrar for all registered Relay Managers. + * + * @notice The client can use an implementation of a `RelayRegistrar` to find relay registration info. + * + */ +interface IRelayRegistrar is IERC165 { + /** + * @notice A struct containing all the information necessary to client to interact with the Relay Server. + */ + struct RelayInfo { + //last registration block number + uint32 lastSeenBlockNumber; + //last registration block timestamp + uint40 lastSeenTimestamp; + //stake (first registration) block number + uint32 firstSeenBlockNumber; + //stake (first registration) block timestamp + uint40 firstSeenTimestamp; + bytes32[3] urlParts; + address relayManager; + } + + /** + * @notice Emitted when a relay server registers or updates its details. + * Looking up these events allows a client to discover registered Relay Servers. + */ + event RelayServerRegistered(address indexed relayManager, address indexed relayHub, bytes32[3] relayUrl); + + /** + * @notice This function is called by Relay Servers in order to register or to update their registration. + * @param relayHub The address of the `RelayHub` contract for which this action is performed. + * @param url The URL of the Relay Server that is listening to the clients' requests. + */ + function registerRelayServer(address relayHub, bytes32[3] calldata url) external; + + /** + * @return The block number in which the contract has been deployed. + */ + function getCreationBlock() external view returns (uint256); + + /** + * @return The maximum age the relay is considered registered by default by this `RelayRegistrar`, in seconds. + */ + function getRelayRegistrationMaxAge() external view returns (uint256); + + /** + * @notice Change the maximum relay registration age. + */ + function setRelayRegistrationMaxAge(uint256) external; + + /** + * @param relayManager An address of a Relay Manager. + * @param relayHub The address of the `RelayHub` contract for which this action is performed. + * @return info All the details of the given Relay Manager's registration. Throws if relay not found for `RelayHub`. + */ + function getRelayInfo(address relayHub, address relayManager) external view returns (RelayInfo memory info); + + /** + * @notice Read relay info of registered Relay Server from an on-chain storage. + * @param relayHub The address of the `RelayHub` contract for which this action is performed. + * @return info The list of `RelayInfo`s of registered Relay Servers + */ + function readRelayInfos(address relayHub) external view returns (RelayInfo[] memory info); + + /** + * @notice Read relay info of registered Relay Server from an on-chain storage. + * @param relayHub The address of the `RelayHub` contract for which this action is performed. + * @param maxCount The maximum amount of relays to be returned by this function. + * @param oldestBlockNumber The latest block number in which a Relay Server may be registered. + * @param oldestBlockTimestamp The latest block timestamp in which a Relay Server may be registered. + * @return info The list of `RelayInfo`s of registered Relay Servers + */ + function readRelayInfosInRange( + address relayHub, + uint256 oldestBlockNumber, + uint256 oldestBlockTimestamp, + uint256 maxCount + ) external view returns (RelayInfo[] memory info); +} diff --git a/contracts/protocol/meta-tx/interfaces/IStakeManager.sol b/contracts/protocol/meta-tx/interfaces/IStakeManager.sol new file mode 100644 index 000000000..f74791d39 --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IStakeManager.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; + +/** + * @title The StakeManager Interface + * @notice In order to prevent an attacker from registering a large number of unresponsive relays, the GSN requires + * the Relay Server to maintain a permanently locked stake in the system before being able to register. + * + * @notice Also, in some cases the behavior of a Relay Server may be found to be illegal by a `Penalizer` contract. + * In such case, the stake will never be returned to the Relay Server operator and will be slashed. + * + * @notice An implementation of this interface is tasked with keeping Relay Servers' stakes, made in any ERC-20 token. + * Note that the `RelayHub` chooses which ERC-20 tokens to support and how much stake is needed. + */ +interface IStakeManager is IERC165 { + /// @notice Emitted when a `stake` or `unstakeDelay` are initialized or increased. + event StakeAdded( + address indexed relayManager, + address indexed owner, + IERC20 token, + uint256 stake, + uint256 unstakeDelay + ); + + /// @notice Emitted once a stake is scheduled for withdrawal. + event StakeUnlocked(address indexed relayManager, address indexed owner, uint256 withdrawTime); + + /// @notice Emitted when owner withdraws `relayManager` funds. + event StakeWithdrawn(address indexed relayManager, address indexed owner, IERC20 token, uint256 amount); + + /// @notice Emitted when an authorized `RelayHub` penalizes a `relayManager`. + event StakePenalized(address indexed relayManager, address indexed beneficiary, IERC20 token, uint256 reward); + + /// @notice Emitted when a `relayManager` adds a new `RelayHub` to a list of authorized. + event HubAuthorized(address indexed relayManager, address indexed relayHub); + + /// @notice Emitted when a `relayManager` removes a `RelayHub` from a list of authorized. + event HubUnauthorized(address indexed relayManager, address indexed relayHub, uint256 removalTime); + + /// @notice Emitted when a `relayManager` sets its `owner`. This is necessary to prevent stake hijacking. + event OwnerSet(address indexed relayManager, address indexed owner); + + /// @notice Emitted when a `burnAddress` is changed. + event BurnAddressSet(address indexed burnAddress); + + /// @notice Emitted when a `devAddress` is changed. + event DevAddressSet(address indexed devAddress); + + /// @notice Emitted if Relay Server is inactive for an `abandonmentDelay` and contract owner initiates its removal. + event RelayServerAbandoned(address indexed relayManager, uint256 abandonedTime); + + /// @notice Emitted to indicate an action performed by a relay server to prevent it from being marked as abandoned. + event RelayServerKeepalive(address indexed relayManager, uint256 keepaliveTime); + + /// @notice Emitted when the stake of an abandoned relayer has been confiscated and transferred to the `devAddress`. + event AbandonedRelayManagerStakeEscheated( + address indexed relayManager, + address indexed owner, + IERC20 token, + uint256 amount + ); + + /** + * @param stake - amount of ether staked for this relay + * @param unstakeDelay - number of seconds to elapse before the owner can retrieve the stake after calling 'unlock' + * @param withdrawTime - timestamp in seconds when 'withdraw' will be callable, or zero if the unlock has not been called + * @param owner - address that receives revenue and manages relayManager's stake + */ + struct StakeInfo { + uint256 stake; + uint256 unstakeDelay; + uint256 withdrawTime; + uint256 abandonedTime; + uint256 keepaliveTime; + IERC20 token; + address owner; + } + + struct RelayHubInfo { + uint256 removalTime; + } + + /** + * @param devAddress - the address that will receive the 'abandoned' stake + * @param abandonmentDelay - the amount of time after which the relay can be marked as 'abandoned' + * @param escheatmentDelay - the amount of time after which the abandoned relay's stake and balance may be withdrawn to the `devAddress` + */ + struct AbandonedRelayServerConfig { + address devAddress; + uint256 abandonmentDelay; + uint256 escheatmentDelay; + } + + /** + * @notice Set the owner of a Relay Manager. Called only by the RelayManager itself. + * Note that owners cannot transfer ownership - if the entry already exists, reverts. + * @param owner - owner of the relay (as configured off-chain) + */ + function setRelayManagerOwner(address owner) external; + + /** + * @notice Put a stake for a relayManager and set its unstake delay. + * Only the owner can call this function. If the entry does not exist, reverts. + * The owner must give allowance of the ERC-20 token to the StakeManager before calling this method. + * It is the RelayHub who has a configurable list of minimum stakes per token. StakeManager accepts all tokens. + * @param token The address of an ERC-20 token that is used by the relayManager as a stake + * @param relayManager The address that represents a stake entry and controls relay registrations on relay hubs + * @param unstakeDelay The number of seconds to elapse before an owner can retrieve the stake after calling `unlock` + * @param amount The amount of tokens to be taken from the relayOwner and locked in the StakeManager as a stake + */ + function stakeForRelayManager( + IERC20 token, + address relayManager, + uint256 unstakeDelay, + uint256 amount + ) external; + + /** + * @notice Schedule the unlocking of the stake. The `unstakeDelay` must pass before owner can call `withdrawStake`. + * @param relayManager The address of a Relay Manager whose stake is to be unlocked. + */ + function unlockStake(address relayManager) external; + + /** + * @notice Withdraw the unlocked stake. + * @param relayManager The address of a Relay Manager whose stake is to be withdrawn. + */ + function withdrawStake(address relayManager) external; + + /** + * @notice Add the `RelayHub` to a list of authorized by this Relay Manager. + * This allows the RelayHub to penalize this Relay Manager. The `RelayHub` cannot trust a Relay it cannot penalize. + * @param relayManager The address of a Relay Manager whose stake is to be authorized for the new `RelayHub`. + * @param relayHub The address of a `RelayHub` to be authorized. + */ + function authorizeHubByOwner(address relayManager, address relayHub) external; + + /** + * @notice Same as `authorizeHubByOwner` but can be called by the RelayManager itself. + */ + function authorizeHubByManager(address relayHub) external; + + /** + * @notice Remove the `RelayHub` from a list of authorized by this Relay Manager. + * @param relayManager The address of a Relay Manager. + * @param relayHub The address of a `RelayHub` to be unauthorized. + */ + function unauthorizeHubByOwner(address relayManager, address relayHub) external; + + /** + * @notice Same as `unauthorizeHubByOwner` but can be called by the RelayManager itself. + */ + function unauthorizeHubByManager(address relayHub) external; + + /** + * Slash the stake of the relay relayManager. In order to prevent stake kidnapping, burns part of stake on the way. + * @param relayManager The address of a Relay Manager to be penalized. + * @param beneficiary The address that receives part of the penalty amount. + * @param amount A total amount of penalty to be withdrawn from stake. + */ + function penalizeRelayManager( + address relayManager, + address beneficiary, + uint256 amount + ) external; + + /** + * @notice Allows the contract owner to set the given `relayManager` as abandoned after a configurable delay. + * Its entire stake and balance will be taken from a relay if it does not respond to being marked as abandoned. + */ + function markRelayAbandoned(address relayManager) external; + + /** + * @notice If more than `abandonmentDelay` has passed since the last Keepalive transaction, and relay manager + * has been marked as abandoned, and after that more that `escheatmentDelay` have passed, entire stake and + * balance will be taken from this relay. + */ + function escheatAbandonedRelayStake(address relayManager) external; + + /** + * @notice Sets a new `keepaliveTime` for the given `relayManager`, preventing it from being marked as abandoned. + * Can be called by an authorized `RelayHub` or by the `relayOwner` address. + */ + function updateRelayKeepaliveTime(address relayManager) external; + + /** + * @notice Check if the Relay Manager can be considered abandoned or not. + * Returns true if the stake's abandonment time is in the past including the escheatment delay, false otherwise. + */ + function isRelayEscheatable(address relayManager) external view returns (bool); + + /** + * @notice Get the stake details information for the given Relay Manager. + * @param relayManager The address of a Relay Manager. + * @return stakeInfo The `StakeInfo` structure. + * @return isSenderAuthorizedHub `true` if the `msg.sender` for this call was a `RelayHub` that is authorized now. + * `false` if the `msg.sender` for this call is not authorized. + */ + function getStakeInfo(address relayManager) + external + view + returns (StakeInfo memory stakeInfo, bool isSenderAuthorizedHub); + + /** + * @return The maximum unstake delay this `StakeManger` allows. This is to prevent locking money forever by mistake. + */ + function getMaxUnstakeDelay() external view returns (uint256); + + /** + * @notice Change the address that will receive the 'burned' part of the penalized stake. + * This is done to prevent malicious Relay Server from penalizing itself and breaking even. + */ + function setBurnAddress(address _burnAddress) external; + + /** + * @return The address that will receive the 'burned' part of the penalized stake. + */ + function getBurnAddress() external view returns (address); + + /** + * @notice Change the address that will receive the 'abandoned' stake. + * This is done to prevent Relay Servers that lost their keys from losing access to funds. + */ + function setDevAddress(address _burnAddress) external; + + /** + * @return The structure that contains all configuration values for the 'abandoned' stake. + */ + function getAbandonedRelayServerConfig() external view returns (AbandonedRelayServerConfig memory); + + /** + * @return the block number in which the contract has been deployed. + */ + function getCreationBlock() external view returns (uint256); + + /** + * @return a SemVer-compliant version of the `StakeManager` contract. + */ + function versionSM() external view returns (string memory); +} diff --git a/contracts/protocol/meta-tx/libraries/GsnEip712Library.sol b/contracts/protocol/meta-tx/libraries/GsnEip712Library.sol new file mode 100644 index 000000000..8cde84790 --- /dev/null +++ b/contracts/protocol/meta-tx/libraries/GsnEip712Library.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "./GsnTypes.sol"; +import "../interfaces/IERC2771Recipient.sol"; +import "../interfaces/IForwarder.sol"; + +import "./GsnUtils.sol"; + +/** + * @title The ERC-712 Library for GSN + * @notice Bridge Library to convert a GSN RelayRequest into a valid `ForwardRequest` for a `Forwarder`. + */ +library GsnEip712Library { + // maximum length of return value/revert reason for 'execute' method. Will truncate result if exceeded. + uint256 private constant MAX_RETURN_SIZE = 1024; + + //copied from Forwarder (can't reference string constants even from another library) + string public constant GENERIC_PARAMS = + "address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime"; + + bytes public constant RELAYDATA_TYPE = + "RelayData(uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,uint256 transactionCalldataGasUsed,address relayWorker,address paymaster,address forwarder,bytes paymasterData,uint256 clientId)"; + + string public constant RELAY_REQUEST_NAME = "RelayRequest"; + string public constant RELAY_REQUEST_SUFFIX = string(abi.encodePacked("RelayData relayData)", RELAYDATA_TYPE)); + + bytes public constant RELAY_REQUEST_TYPE = + abi.encodePacked(RELAY_REQUEST_NAME, "(", GENERIC_PARAMS, ",", RELAY_REQUEST_SUFFIX); + + bytes32 public constant RELAYDATA_TYPEHASH = keccak256(RELAYDATA_TYPE); + bytes32 public constant RELAY_REQUEST_TYPEHASH = keccak256(RELAY_REQUEST_TYPE); + + struct EIP712Domain { + string name; + string version; + uint256 chainId; + address verifyingContract; + } + + bytes32 public constant EIP712DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + function splitRequest(GsnTypes.RelayRequest calldata req) internal pure returns (bytes memory suffixData) { + suffixData = abi.encode(hashRelayData(req.relayData)); + } + + //verify that the recipient trusts the given forwarder + // MUST be called by paymaster + function verifyForwarderTrusted(GsnTypes.RelayRequest calldata relayRequest) internal view { + (bool success, bytes memory ret) = + relayRequest.request.to.staticcall( + abi.encodeWithSelector(IERC2771Recipient.isTrustedForwarder.selector, relayRequest.relayData.forwarder) + ); + require(success, "isTrustedForwarder: reverted"); + require(ret.length == 32, "isTrustedForwarder: bad response"); + require(abi.decode(ret, (bool)), "invalid forwarder for recipient"); + } + + function verifySignature( + string memory domainSeparatorName, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) internal view { + bytes memory suffixData = splitRequest(relayRequest); + bytes32 _domainSeparator = domainSeparator(domainSeparatorName, relayRequest.relayData.forwarder); + IForwarder forwarder = IForwarder(payable(relayRequest.relayData.forwarder)); + forwarder.verify(relayRequest.request, _domainSeparator, RELAY_REQUEST_TYPEHASH, suffixData, signature); + } + + function verify( + string memory domainSeparatorName, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) internal view { + verifyForwarderTrusted(relayRequest); + verifySignature(domainSeparatorName, relayRequest, signature); + } + + function execute( + string memory domainSeparatorName, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) + internal + returns ( + bool forwarderSuccess, + bool callSuccess, + bytes memory ret + ) + { + bytes memory suffixData = splitRequest(relayRequest); + bytes32 _domainSeparator = domainSeparator(domainSeparatorName, relayRequest.relayData.forwarder); + /* solhint-disable-next-line avoid-low-level-calls */ + (forwarderSuccess, ret) = relayRequest.relayData.forwarder.call( + abi.encodeWithSelector( + IForwarder.execute.selector, + relayRequest.request, + _domainSeparator, + RELAY_REQUEST_TYPEHASH, + suffixData, + signature + ) + ); + if (forwarderSuccess) { + //decode return value of execute: + (callSuccess, ret) = abi.decode(ret, (bool, bytes)); + } + truncateInPlace(ret); + } + + //truncate the given parameter (in-place) if its length is above the given maximum length + // do nothing otherwise. + //NOTE: solidity warns unless the method is marked "pure", but it DOES modify its parameter. + function truncateInPlace(bytes memory data) internal pure { + MinLibBytes.truncateInPlace(data, MAX_RETURN_SIZE); + } + + function domainSeparator(string memory name, address forwarder) internal view returns (bytes32) { + return + hashDomain(EIP712Domain({ name: name, version: "3", chainId: getChainID(), verifyingContract: forwarder })); + } + + function getChainID() internal view returns (uint256 id) { + /* solhint-disable no-inline-assembly */ + assembly { + id := chainid() + } + } + + function hashDomain(EIP712Domain memory req) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + EIP712DOMAIN_TYPEHASH, + keccak256(bytes(req.name)), + keccak256(bytes(req.version)), + req.chainId, + req.verifyingContract + ) + ); + } + + function hashRelayData(GsnTypes.RelayData calldata req) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + RELAYDATA_TYPEHASH, + req.maxFeePerGas, + req.maxPriorityFeePerGas, + req.transactionCalldataGasUsed, + req.relayWorker, + req.paymaster, + req.forwarder, + keccak256(req.paymasterData), + req.clientId + ) + ); + } +} diff --git a/contracts/protocol/meta-tx/libraries/GsnTypes.sol b/contracts/protocol/meta-tx/libraries/GsnTypes.sol new file mode 100644 index 000000000..b4d498813 --- /dev/null +++ b/contracts/protocol/meta-tx/libraries/GsnTypes.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier:MIT + +pragma solidity ^0.8.0; + +import "../interfaces/IForwarder.sol"; + +library GsnTypes { + struct RelayData { + uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; + uint256 transactionCalldataGasUsed; + address relayWorker; + address paymaster; + address forwarder; + bytes paymasterData; + uint256 clientId; + } + + struct RelayRequest { + IForwarder.ForwardRequest request; + RelayData relayData; + } +} diff --git a/contracts/protocol/meta-tx/libraries/GsnUtils.sol b/contracts/protocol/meta-tx/libraries/GsnUtils.sol new file mode 100644 index 000000000..6e67004a9 --- /dev/null +++ b/contracts/protocol/meta-tx/libraries/GsnUtils.sol @@ -0,0 +1,57 @@ +/* solhint-disable no-inline-assembly */ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; + +import "./MinLibBytes.sol"; +import "./GsnTypes.sol"; + +/** + * @title The GSN Solidity Utils Library + * @notice Some library functions used throughout the GSN Solidity codebase. + */ +library GsnUtils { + bytes32 private constant RELAY_REQUEST_ID_MASK = 0x00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + + /** + * @notice Calculate an identifier for the meta-transaction in a format similar to a transaction hash. + * Note that uniqueness relies on signature and may not be enforced if meta-transactions are verified + * with a different algorithm, e.g. when batching. + * @param relayRequest The `RelayRequest` for which an ID is being calculated. + * @param signature The signature for the `RelayRequest`. It is not validated here and may even remain empty. + */ + function getRelayRequestID(GsnTypes.RelayRequest calldata relayRequest, bytes calldata signature) + internal + pure + returns (bytes32) + { + return + keccak256(abi.encode(relayRequest.request.from, relayRequest.request.nonce, signature)) & + RELAY_REQUEST_ID_MASK; + } + + /** + * @notice Extract the method identifier signature from the encoded function call. + */ + function getMethodSig(bytes memory msgData) internal pure returns (bytes4) { + return MinLibBytes.readBytes4(msgData, 0); + } + + /** + * @notice Extract a parameter from encoded-function block. + * see: https://solidity.readthedocs.io/en/develop/abi-spec.html#formal-specification-of-the-encoding + * The return value should be casted to the right type (`uintXXX`/`bytesXXX`/`address`/`bool`/`enum`). + * @param msgData Byte array containing a uint256 value. + * @param index Index in byte array of uint256 value. + * @return result uint256 value from byte array. + */ + function getParam(bytes memory msgData, uint256 index) internal pure returns (uint256 result) { + return MinLibBytes.readUint256(msgData, 4 + index * 32); + } + + /// @notice Re-throw revert with the same revert data. + function revertWithData(bytes memory data) internal pure { + assembly { + revert(add(data, 32), mload(data)) + } + } +} diff --git a/contracts/protocol/meta-tx/libraries/MinLibBytes.sol b/contracts/protocol/meta-tx/libraries/MinLibBytes.sol new file mode 100644 index 000000000..94360b168 --- /dev/null +++ b/contracts/protocol/meta-tx/libraries/MinLibBytes.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +// minimal bytes manipulation required by GSN +// a minimal subset from 0x/LibBytes +/* solhint-disable no-inline-assembly */ +pragma solidity ^0.8.0; + +library MinLibBytes { + //truncate the given parameter (in-place) if its length is above the given maximum length + // do nothing otherwise. + //NOTE: solidity warns unless the method is marked "pure", but it DOES modify its parameter. + function truncateInPlace(bytes memory data, uint256 maxlen) internal pure { + if (data.length > maxlen) { + assembly { + mstore(data, maxlen) + } + } + } + + /// @dev Reads an address from a position in a byte array. + /// @param b Byte array containing an address. + /// @param index Index in byte array of address. + /// @return result address from byte array. + function readAddress(bytes memory b, uint256 index) internal pure returns (address result) { + require(b.length >= index + 20, "readAddress: data too short"); + + // Add offset to index: + // 1. Arrays are prefixed by 32-byte length parameter (add 32 to index) + // 2. Account for size difference between address length and 32-byte storage word (subtract 12 from index) + index += 20; + + // Read address from array memory + assembly { + // 1. Add index to address of bytes array + // 2. Load 32-byte word from memory + // 3. Apply 20-byte mask to obtain address + result := and(mload(add(b, index)), 0xffffffffffffffffffffffffffffffffffffffff) + } + return result; + } + + function readBytes32(bytes memory b, uint256 index) internal pure returns (bytes32 result) { + require(b.length >= index + 32, "readBytes32: data too short"); + + // Read the bytes32 from array memory + assembly { + result := mload(add(b, add(index, 32))) + } + return result; + } + + /// @dev Reads a uint256 value from a position in a byte array. + /// @param b Byte array containing a uint256 value. + /// @param index Index in byte array of uint256 value. + /// @return result uint256 value from byte array. + function readUint256(bytes memory b, uint256 index) internal pure returns (uint256 result) { + result = uint256(readBytes32(b, index)); + return result; + } + + function readBytes4(bytes memory b, uint256 index) internal pure returns (bytes4 result) { + require(b.length >= index + 4, "readBytes4: data too short"); + + // Read the bytes4 from array memory + assembly { + result := mload(add(b, add(index, 32))) + // Solidity does not require us to clean the trailing bytes. + // We do it anyway + result := and(result, 0xFFFFFFFF00000000000000000000000000000000000000000000000000000000) + } + return result; + } +} From 0dd050135e0ae6a818984ff8f060e43715048b74 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Tue, 27 Sep 2022 21:10:50 -0400 Subject: [PATCH 12/30] feat(interfaces): vault interface --- .../meta-tx/interfaces/IERC20PermitLegacy.sol | 15 ++++++++ .../protocol/meta-tx/interfaces/IVault.sol | 38 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 contracts/protocol/meta-tx/interfaces/IERC20PermitLegacy.sol create mode 100644 contracts/protocol/meta-tx/interfaces/IVault.sol diff --git a/contracts/protocol/meta-tx/interfaces/IERC20PermitLegacy.sol b/contracts/protocol/meta-tx/interfaces/IERC20PermitLegacy.sol new file mode 100644 index 000000000..0bdbe7cc9 --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IERC20PermitLegacy.sol @@ -0,0 +1,15 @@ +//SPDX-license-identifier: MIT +pragma solidity ^0.8.15; + +interface IERC20PermitLegacy { + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/contracts/protocol/meta-tx/interfaces/IVault.sol b/contracts/protocol/meta-tx/interfaces/IVault.sol new file mode 100644 index 000000000..797acc965 --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IVault.sol @@ -0,0 +1,38 @@ +//SPDX-license-identifier: MIT +pragma solidity ^0.8.15; + +interface IVault { + /** + * @notice Deposit underlying tokens to the vault + * @dev Mint the shares right away as per oracle based price per full share value + * @param _beneficiary the address of the deposit beneficiary, + * if _beneficiary = address(0) => _beneficiary = msg.sender + * @param _userDepositUT Amount in underlying token + * @param _permitParams permit parameters: amount, deadline, v, s, r + * @param _accountsProof merkle proof for caller + */ + function userDepositVault( + address _beneficiary, + uint256 _userDepositUT, + bytes calldata _permitParams, + bytes32[] calldata _accountsProof + ) external returns (uint256); + + /** + * @notice redeems the vault shares and transfers underlying token to `_beneficiary` + * @dev Burn the shares right away as per oracle based price per full share value + * @param _receiver the address which will receive the underlying tokens + * @param _userWithdrawVT amount in vault token + * @param _accountsProof merkle proof for caller + */ + function userWithdrawVault( + address _receiver, + uint256 _userWithdrawVT, + bytes32[] calldata _accountsProof + ) external returns (uint256); + + /** + * @dev the vault underlying token contract address + */ + function underlyingToken() external returns (address); +} From 48ef024db59338ef32501b9f4ec84b88766dd262 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Tue, 27 Sep 2022 21:53:02 -0400 Subject: [PATCH 13/30] fix(interface): fix ivault --- contracts/protocol/meta-tx/interfaces/IVault.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/protocol/meta-tx/interfaces/IVault.sol b/contracts/protocol/meta-tx/interfaces/IVault.sol index 797acc965..22712285d 100644 --- a/contracts/protocol/meta-tx/interfaces/IVault.sol +++ b/contracts/protocol/meta-tx/interfaces/IVault.sol @@ -8,12 +8,14 @@ interface IVault { * @param _beneficiary the address of the deposit beneficiary, * if _beneficiary = address(0) => _beneficiary = msg.sender * @param _userDepositUT Amount in underlying token + * @param _expectedOutput expected output * @param _permitParams permit parameters: amount, deadline, v, s, r * @param _accountsProof merkle proof for caller */ function userDepositVault( address _beneficiary, uint256 _userDepositUT, + uint256 _expectedOutput, bytes calldata _permitParams, bytes32[] calldata _accountsProof ) external returns (uint256); @@ -23,11 +25,13 @@ interface IVault { * @dev Burn the shares right away as per oracle based price per full share value * @param _receiver the address which will receive the underlying tokens * @param _userWithdrawVT amount in vault token + * @param _expectedOutput expected output * @param _accountsProof merkle proof for caller */ function userWithdrawVault( address _receiver, uint256 _userWithdrawVT, + uint256 _expectedOutput, bytes32[] calldata _accountsProof ) external returns (uint256); From df8340234cc2d16318df6f3b2ebdf122d82dab76 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Tue, 27 Sep 2022 21:53:47 -0400 Subject: [PATCH 14/30] feat(paymaster): token paymaster contract --- contracts/protocol/meta-tx/TokenPaymaster.sol | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 contracts/protocol/meta-tx/TokenPaymaster.sol diff --git a/contracts/protocol/meta-tx/TokenPaymaster.sol b/contracts/protocol/meta-tx/TokenPaymaster.sol new file mode 100644 index 000000000..70ddef181 --- /dev/null +++ b/contracts/protocol/meta-tx/TokenPaymaster.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +//contracts +import { Ownable } from "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; +import { ERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/ERC20.sol"; +import { BasePaymaster } from "./BasePaymaster.sol"; + +//libraries +import { SafeMath } from "@openzeppelin/contracts-0.8.x/utils/math/SafeMath.sol"; +import { SafeERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; +import { GsnTypes } from "./libraries/GsnTypes.sol"; +import { DataTypes as SwapTypes } from "../optyfi-swapper/contracts/swap/DataTypes.sol"; + +//interfaces +import { ISwapper } from "../optyfi-swapper/contracts/swap/ISwapper.sol"; +import { IOptyFiOracle } from "../optyfi-oracle/contracts/interfaces/IOptyFiOracle.sol"; +import { IERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; +import { IForwarder } from "./interfaces/IForwarder.sol"; + +/** + * @title A Token-based paymaster + * @author opty.fi + * @notice - each request is paid for by the caller. + * - preRelayedCall - pre-pay the maximum possible price for the tx + * - postRelayedCall - refund the caller for the unused gas + */ +contract TokenPaymaster is BasePaymaster { + using SafeERC20 for IERC20; + using SafeMath for uint256; + + /** @dev ETH address */ + address private constant ETH = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + + /** WETH ERC20 token address */ + address public constant WETH = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + ISwapper optyFiSwapper; + IOptyFiOracle optyFiOracle; + uint256 public gasUsedByPost; + + constructor(address _optyFiswapper, address _optyFiOracle) { + optyFiOracle = IOptyFiOracle(_optyFiOracle); + optyFiSwapper = ISwapper(_optyFiswapper); + } + + function versionPaymaster() external view virtual override returns (string memory) { + return "2.2.3+opengsn.token.ipaymaster"; + } + + /** + * @notice set gas used by postRelayedCall, for proper gas calculation. + * You can use TokenGasCalculator to calculate these values (they depend on actual code of postRelayedCall, + * but also the gas usage of the token and the swap) + * @param _gasUsedByPost gas used by postRelayedCall + */ + function setPostGasUsage(uint256 _gasUsedByPost) external onlyOwner { + gasUsedByPost = _gasUsedByPost; + } + + /** + * @notice Sets the OptyFi Oracle contract + * @param _optyFiOracle OptyFi Oracle contract address + */ + function setOptyFiOracle(address _optyFiOracle) external onlyOwner { + optyFiOracle = IOptyFiOracle(_optyFiOracle); + } + + /** + * @notice Sets the OptyFi Swapper contract + * @param _optyFiSwapper OptyFi Swapper contract address + */ + function setOptyFiSwapper(address _optyFiSwapper) external onlyOwner { + optyFiSwapper = ISwapper(_optyFiSwapper); + } + + /** + * @notice return the payer of a given RelayRequest + * @param _relayRequest GSN RelayRequest + */ + function getPayer(GsnTypes.RelayRequest calldata _relayRequest) public view virtual returns (address) { + return _relayRequest.request.to; + } + + /** + * @dev get the user's input token + * @param _paymasterData paymaster data containing the swap data + */ + function _getToken(bytes memory _paymasterData) internal view returns (IERC20 token) { + (address _token, , , , , , ) = + abi.decode(_paymasterData, (address, address[], bytes, uint256[], uint256[], bytes, uint256)); + token = IERC20(token); + } + + /** + * @dev decode paymasterData + * @param _paymasterData swap data (token, callees, exchangeData, startIndexes, values, permit, deadline) + */ + function _getSwapData(bytes memory _paymasterData) + internal + view + returns ( + address token, + address[] memory callees, + bytes memory exchangeData, + uint256[] memory startIndexes, + uint256[] memory values, + bytes memory permit, + uint256 deadline + ) + { + (token, callees, exchangeData, startIndexes, values, permit, deadline) = abi.decode( + _paymasterData, + (address, address[], bytes, uint256[], uint256[], bytes, uint256) + ); + } + + /** + * @dev return the pre charge value + * @param _token token used to pay for gas + * @param _relayRequest forward request and relay data + * @param _maxPossibleGas max possible gas to spent + */ + function _calculatePreCharge( + address _token, + GsnTypes.RelayRequest calldata _relayRequest, + uint256 _maxPossibleGas + ) internal view returns (address payer, uint256 tokenPreCharge) { + payer = this.getPayer(_relayRequest); + uint256 ethMaxCharge = relayHub.calculateCharge(_maxPossibleGas, _relayRequest.relayData); + ethMaxCharge += _relayRequest.request.value; + tokenPreCharge = _getETHInToken(_token, ethMaxCharge); + } + + /** + * @dev calculates pre charge value and transfers to paymaster + * @param _relayRequest forward request and relay data + * @param _signature payer signature + * @param _approvalData token approvals + * @param _maxPossibleGas max possible gas to spent + */ + function _preRelayedCall( + GsnTypes.RelayRequest calldata _relayRequest, + bytes calldata _signature, + bytes calldata _approvalData, + uint256 _maxPossibleGas + ) internal virtual override returns (bytes memory context, bool revertOnRecipientRevert) { + (_signature, _approvalData); + + IERC20 token = _getToken(_relayRequest.relayData.paymasterData); + (address payer, uint256 tokenPrecharge) = _calculatePreCharge(address(token), _relayRequest, _maxPossibleGas); + token.safeTransferFrom(payer, address(this), tokenPrecharge); + return (abi.encode(payer, tokenPrecharge, token), false); + } + + /** + * @dev calculates actual gas spent, refund relayer for gas the spent, and refund payer for unspent tokens + * @param _context context bytes + * @param _gasUseWithoutPost actual gas used without post relayed call cost + * @param _relayData relay data + */ + function _postRelayedCall( + bytes calldata _context, + bool, + uint256 _gasUseWithoutPost, + GsnTypes.RelayData calldata _relayData + ) internal virtual override { + (address payer, uint256 tokenPrecharge, IERC20 token) = abi.decode(_context, (address, uint256, IERC20)); + _postRelayedCallInternal(payer, tokenPrecharge, 0, _gasUseWithoutPost, _relayData, token); + } + + /** + * @dev calculates actual gas spent, refund relayer for gas the spent, and refund payer for unspent tokens + * @param _payer original payer address + * @param _tokenPrecharge max gas spent in token unit + * @param _valueRequested value requested + * @param _gasUseWithoutPost actual gas used without post relayed call cost + * @param _relayData relay data + * @param _token token used to pay for gas + */ + function _postRelayedCallInternal( + address _payer, + uint256 _tokenPrecharge, + uint256 _valueRequested, + uint256 _gasUseWithoutPost, + GsnTypes.RelayData calldata _relayData, + IERC20 _token + ) internal { + uint256 ethActualCharge = relayHub.calculateCharge(_gasUseWithoutPost.add(gasUsedByPost), _relayData); + uint256 tokenActualCharge = _getETHInToken(address(_token), _valueRequested.add(ethActualCharge)); + uint256 tokenRefund = _tokenPrecharge.sub(tokenActualCharge); + _refundPayer(_payer, _token, tokenRefund); + _depositProceedsToHub(ethActualCharge, _relayData.paymasterData); + emit TokensCharged(_gasUseWithoutPost, gasUsedByPost, ethActualCharge, tokenActualCharge); + } + + /** + * @dev refund payer for the unspent tokens + * @param _payer address + * @param _token token used to pay for gas + * @param _tokenRefund amount to refund + */ + function _refundPayer( + address _payer, + IERC20 _token, + uint256 _tokenRefund + ) private { + require(_token.transfer(_payer, _tokenRefund), "failed refund"); + } + + /** + * @dev refund relayer for gas the spent + * @param _ethActualCharge gas cost paid by the relayer + * @param _paymasterData swap data (token, callees, exchangeData, startIndexes, values, permit, deadline) + */ + function _depositProceedsToHub(uint256 _ethActualCharge, bytes calldata _paymasterData) private { + uint256 receivedAmount = _swapToETH(_ethActualCharge, _paymasterData); + relayHub.depositFor{ value: receivedAmount }(address(this)); + } + + /** + * @dev swap token for ETH + * @param _ethActualCharge gas cost paid by the relayer + * @param _paymasterData swap data (token, callees, exchangeData, startIndexes, values, permit, deadline) + * @return receivedAmount amount received from the swap + */ + function _swapToETH(uint256 _ethActualCharge, bytes calldata _paymasterData) + private + returns (uint256 receivedAmount) + { + ( + address token, + address[] memory callees, + bytes memory exchangeData, + uint256[] memory startIndexes, + uint256[] memory values, + bytes memory permit, + uint256 deadline + ) = _getSwapData(_paymasterData); + uint256 tokenAmount = _getETHInToken(token, _ethActualCharge); + SwapTypes.SwapData memory swapData = + SwapTypes.SwapData( + token, + ETH, + tokenAmount, + _ethActualCharge, + callees, + exchangeData, + startIndexes, + values, + payable(address(this)), + permit, + deadline + ); + uint256 unused; + (receivedAmount, unused) = optyFiSwapper.swap(swapData); + if (unused > 0) { + IERC20(token).safeTransfer(msg.sender, unused); + } + } + + /** + * @dev Get the expected amount to receive of _token1 after swapping _token0 + * @param _swapInAmount Amount of _token0 to be swapped for _token1 + * @param _token0 Contract address of one of the liquidity pool's underlying tokens + * @param _token1 Contract address of one of the liquidity pool's underlying tokens + * @return _swapOutAmount oracle price + */ + function _calculateSwapOutAmount( + uint256 _swapInAmount, + address _token0, + address _token1 + ) internal view returns (uint256 _swapOutAmount) { + uint256 price = optyFiOracle.getTokenPrice(_token0, _token1); + require(price > uint256(0), "!price"); + uint256 decimals0 = uint256(ERC20(_token0).decimals()); + uint256 decimals1 = uint256(ERC20(_token1).decimals()); + _swapOutAmount = ((_swapInAmount * price * 10**decimals1) / 10**(18 + decimals0)); + } + + /** + * @dev Get the _token equivalent amount of WETH + * @param _token token address + * @param _amount amount in WETH + * @return amount token equivalent amount + */ + function _getETHInToken(address _token, uint256 _amount) internal view returns (uint256 amount) { + _token == WETH ? amount = _amount : amount = _calculateSwapOutAmount(_amount, WETH, _token); + } + + receive() external payable override { + emit Received(msg.value); + } + + event Received(uint256 eth); + + event TokensCharged( + uint256 gasUseWithoutPost, + uint256 gasJustPost, + uint256 ethActualCharge, + uint256 tokenActualCharge + ); +} From 46b13a1b4cfb3326adbc0dae6fb3456f2d1235a1 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Tue, 27 Sep 2022 21:58:27 -0400 Subject: [PATCH 15/30] feat(yarn lock): add opengsn --- yarn.lock | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/yarn.lock b/yarn.lock index 7c95dab4a..79c483b00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1778,6 +1778,16 @@ __metadata: languageName: node linkType: hard +"@opengsn/contracts@npm:^3.0.0-beta.1": + version: 3.0.0-beta.2 + resolution: "@opengsn/contracts@npm:3.0.0-beta.2" + dependencies: + "@openzeppelin/contracts": ^4.2.0 + web3-eth-contract: ^1.7.4 + checksum: c44562d4d34b32b31755c2d18fe380584d23618d8d280fb47c9e20c053d0e479274aa3026628575ddf64eaba398c021961c429d940c9cb4cfc801534fdf517b2 + languageName: node + linkType: hard + "@openzeppelin/contracts-0.8.x@npm:@openzeppelin/contracts@4.4.1": version: 4.4.1 resolution: "@openzeppelin/contracts@npm:4.4.1" @@ -1792,6 +1802,13 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts@npm:^4.2.0": + version: 4.7.3 + resolution: "@openzeppelin/contracts@npm:4.7.3" + checksum: 18382fcacf7cfd652f5dd0e70c08f08ea74eaa8ff11e9f9850639ada70198ae01a3f9493d89a52d724f2db394e9616bf6258017804612ba273167cf657fbb073 + languageName: node + linkType: hard + "@optyfi/defi-legos@npm:v0.1.0-rc.38": version: 0.1.0-rc.38 resolution: "@optyfi/defi-legos@npm:0.1.0-rc.38" @@ -1962,6 +1979,13 @@ __metadata: languageName: node linkType: hard +"@solidstate/contracts@npm:^0.0.35": + version: 0.0.35 + resolution: "@solidstate/contracts@npm:0.0.35" + checksum: db0a88d6c01dd99f40cc8074d3df429d8485fa997eb38171c18464253a4d7512b2f04c925efd0ef2373691a875c063c52cc7f382232995f67984fd4fc7cb9974 + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^1.1.2": version: 1.1.2 resolution: "@szmarczak/http-timer@npm:1.1.2" @@ -2774,6 +2798,13 @@ __metadata: languageName: node linkType: hard +"abortcontroller-polyfill@npm:^1.7.3": + version: 1.7.3 + resolution: "abortcontroller-polyfill@npm:1.7.3" + checksum: 55739d7f0c9bd6afa2aabb3148778967c4dd4dcff91f6b9259df38da34f9882d3f7730b0954e9767a19cc16a8dd9a58915da4e8a50220300d45af3817d7557b1 + languageName: node + linkType: hard + "abstract-leveldown@npm:3.0.0": version: 3.0.0 resolution: "abstract-leveldown@npm:3.0.0" @@ -5589,6 +5620,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^3.1.4": + version: 3.1.5 + resolution: "cross-fetch@npm:3.1.5" + dependencies: + node-fetch: 2.6.7 + checksum: f6b8c6ee3ef993ace6277fd789c71b6acf1b504fd5f5c7128df4ef2f125a429e29cd62dc8c127523f04a5f2fa4771ed80e3f3d9695617f441425045f505cf3bb + languageName: node + linkType: hard + "cross-spawn@npm:^6.0.0, cross-spawn@npm:^6.0.5": version: 6.0.5 resolution: "cross-spawn@npm:6.0.5" @@ -6244,9 +6284,11 @@ __metadata: "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@0.3.0-beta.12" "@nomiclabs/hardhat-etherscan": ^3.0.1 "@nomiclabs/hardhat-waffle": ^2.0.1 + "@opengsn/contracts": ^3.0.0-beta.1 "@openzeppelin/contracts": 3.4.0 "@openzeppelin/contracts-0.8.x": "npm:@openzeppelin/contracts@4.4.1" "@optyfi/defi-legos": v0.1.0-rc.38 + "@solidstate/contracts": ^0.0.35 "@tenderly/hardhat-tenderly": 1.1.0-beta.6 "@typechain/ethers-v5": ^7.1.2 "@typechain/hardhat": ^2.3.0 @@ -6550,6 +6592,13 @@ __metadata: languageName: node linkType: hard +"es6-promise@npm:^4.2.8": + version: 4.2.8 + resolution: "es6-promise@npm:4.2.8" + checksum: 95614a88873611cb9165a85d36afa7268af5c03a378b35ca7bda9508e1d4f1f6f19a788d4bc755b3fd37c8ebba40782018e02034564ff24c9d6fa37e959ad57d + languageName: node + linkType: hard + "es6-symbol@npm:^3.1.1, es6-symbol@npm:~3.1.3": version: 3.1.3 resolution: "es6-symbol@npm:3.1.3" @@ -12208,6 +12257,20 @@ fsevents@~2.3.2: languageName: node linkType: hard +"node-fetch@npm:2.6.7": + version: 2.6.7 + resolution: "node-fetch@npm:2.6.7" + dependencies: + whatwg-url: ^5.0.0 + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 8d816ffd1ee22cab8301c7756ef04f3437f18dace86a1dae22cf81db8ef29c0bf6655f3215cb0cdb22b420b6fe141e64b26905e7f33f9377a7fa59135ea3e10b + languageName: node + linkType: hard + "node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1": version: 2.6.5 resolution: "node-fetch@npm:2.6.5" @@ -16894,6 +16957,16 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core-helpers@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core-helpers@npm:1.8.0" + dependencies: + web3-eth-iban: 1.8.0 + web3-utils: 1.8.0 + checksum: f0af1cfb790b2c51ac29d19d0c77cc4caf8e363ffc069b4943bbd4ebf602bd04835af6c5c4f8b2601ee3c8157b8c658783a7ddf3cd82a9c1d9c15091a624249a + languageName: node + linkType: hard + "web3-core-method@npm:1.2.11": version: 1.2.11 resolution: "web3-core-method@npm:1.2.11" @@ -16935,6 +17008,19 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core-method@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core-method@npm:1.8.0" + dependencies: + "@ethersproject/transactions": ^5.6.2 + web3-core-helpers: 1.8.0 + web3-core-promievent: 1.8.0 + web3-core-subscriptions: 1.8.0 + web3-utils: 1.8.0 + checksum: 5b73af8a34c94cfaee8dd69435fe78cc6eccde2e30c2d9632b4efa96d18b8ee13012d14ed6cfe4350db6f9ea2e04c53e55a47ac31db013728812427828338290 + languageName: node + linkType: hard + "web3-core-promievent@npm:1.2.11": version: 1.2.11 resolution: "web3-core-promievent@npm:1.2.11" @@ -16962,6 +17048,15 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core-promievent@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core-promievent@npm:1.8.0" + dependencies: + eventemitter3: 4.0.4 + checksum: 0c39987322104e9243c79a33effbeeb9380dfc4ae711a7a07704bdd1610cf23b46dcb5a50164df0065159c5fb2f336f5700c522feff2299de8a12f431e1884b7 + languageName: node + linkType: hard + "web3-core-requestmanager@npm:1.2.11": version: 1.2.11 resolution: "web3-core-requestmanager@npm:1.2.11" @@ -17001,6 +17096,19 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core-requestmanager@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core-requestmanager@npm:1.8.0" + dependencies: + util: ^0.12.0 + web3-core-helpers: 1.8.0 + web3-providers-http: 1.8.0 + web3-providers-ipc: 1.8.0 + web3-providers-ws: 1.8.0 + checksum: df4d8295a9b0e328c63498c5cc8a828d2f1d7f7e6f6893daa1f4b1ce2de5ed9fd428838606ec267f47c83b610340909c30975e51ab7e07564a1ec598043fb3ba + languageName: node + linkType: hard + "web3-core-subscriptions@npm:1.2.11": version: 1.2.11 resolution: "web3-core-subscriptions@npm:1.2.11" @@ -17032,6 +17140,16 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core-subscriptions@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core-subscriptions@npm:1.8.0" + dependencies: + eventemitter3: 4.0.4 + web3-core-helpers: 1.8.0 + checksum: 934d3b823b7ca7859509f806c81d236bd0e93224e91b9928538a669e6a42e95aa0d3117f36f54f2d4c5e5f429748eb011f9e99022edf363a48d1c5c1e40da212 + languageName: node + linkType: hard + "web3-core@npm:1.2.11": version: 1.2.11 resolution: "web3-core@npm:1.2.11" @@ -17062,6 +17180,21 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core@npm:1.8.0" + dependencies: + "@types/bn.js": ^5.1.0 + "@types/node": ^12.12.6 + bignumber.js: ^9.0.0 + web3-core-helpers: 1.8.0 + web3-core-method: 1.8.0 + web3-core-requestmanager: 1.8.0 + web3-utils: 1.8.0 + checksum: a524cc2b23af54650166af07d30a242ec740fe8274bec72e8f2b1757deaf733de295eb6c71594e2332b46a32f69e060414b138a5a4e7598e503188c96a0efa44 + languageName: node + linkType: hard + "web3-core@npm:^1.7.1": version: 1.7.3 resolution: "web3-core@npm:1.7.3" @@ -17098,6 +17231,16 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-eth-abi@npm:1.8.0": + version: 1.8.0 + resolution: "web3-eth-abi@npm:1.8.0" + dependencies: + "@ethersproject/abi": ^5.6.3 + web3-utils: 1.8.0 + checksum: c9559abc6e928017a561e619863735f5d0c04efe93aee484cff5a376d780be4af40ff7a67723e4478c2e174ad8f35d2a102dd7c442cc976fa374c60c7607445b + languageName: node + linkType: hard + "web3-eth-accounts@npm:1.2.11": version: 1.2.11 resolution: "web3-eth-accounts@npm:1.2.11" @@ -17169,6 +17312,22 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-eth-contract@npm:^1.7.4": + version: 1.8.0 + resolution: "web3-eth-contract@npm:1.8.0" + dependencies: + "@types/bn.js": ^5.1.0 + web3-core: 1.8.0 + web3-core-helpers: 1.8.0 + web3-core-method: 1.8.0 + web3-core-promievent: 1.8.0 + web3-core-subscriptions: 1.8.0 + web3-eth-abi: 1.8.0 + web3-utils: 1.8.0 + checksum: d433984796c2ac5a4a436d5e9cc93b89e3d4669ec332730c628d471fc23981d19be3a8cea1cfe8634f17469da7e33fa6302cb3d886c2a7d0322bc2e03d8d6a59 + languageName: node + linkType: hard + "web3-eth-ens@npm:1.2.11": version: 1.2.11 resolution: "web3-eth-ens@npm:1.2.11" @@ -17232,6 +17391,16 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-eth-iban@npm:1.8.0": + version: 1.8.0 + resolution: "web3-eth-iban@npm:1.8.0" + dependencies: + bn.js: ^5.2.1 + web3-utils: 1.8.0 + checksum: 3877b18da7c4a965a8cf180fdff95bc3de9a94f671e336403cb11c45b646257fb4c26b3f1e18a51a37f5865190c74f769354a5719517324997bc6a43e2ac2834 + languageName: node + linkType: hard + "web3-eth-personal@npm:1.2.11": version: 1.2.11 resolution: "web3-eth-personal@npm:1.2.11" @@ -17381,6 +17550,18 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-providers-http@npm:1.8.0": + version: 1.8.0 + resolution: "web3-providers-http@npm:1.8.0" + dependencies: + abortcontroller-polyfill: ^1.7.3 + cross-fetch: ^3.1.4 + es6-promise: ^4.2.8 + web3-core-helpers: 1.8.0 + checksum: 2af709826ae806c02db1936ccfd25bc28be04403c2e41a02a5e8ebb120ba910cf97dd8c8203c1d08d110a819f7a6548cca813227f4bb567a733d92c4a3ebc930 + languageName: node + linkType: hard + "web3-providers-ipc@npm:1.2.11": version: 1.2.11 resolution: "web3-providers-ipc@npm:1.2.11" @@ -17412,6 +17593,16 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-providers-ipc@npm:1.8.0": + version: 1.8.0 + resolution: "web3-providers-ipc@npm:1.8.0" + dependencies: + oboe: 2.1.5 + web3-core-helpers: 1.8.0 + checksum: b65d7f0f37c0d477d46f7e6db7c2ddad0d9bf3ff9a37cdb0b220788779bba551086e11af16ee2488d397d7f54cb16795bddf6b96435ed2527c495391a70b8993 + languageName: node + linkType: hard + "web3-providers-ws@npm:1.2.11": version: 1.2.11 resolution: "web3-providers-ws@npm:1.2.11" @@ -17446,6 +17637,17 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-providers-ws@npm:1.8.0": + version: 1.8.0 + resolution: "web3-providers-ws@npm:1.8.0" + dependencies: + eventemitter3: 4.0.4 + web3-core-helpers: 1.8.0 + websocket: ^1.0.32 + checksum: 61124ae1c6e68713553083a2d652169d399e368c6a9f49b4966cf5c59bcaee2a47e55d993726b3702a605ed48d8b18d79398712cd5b67ac237ec46715033e7fc + languageName: node + linkType: hard + "web3-shh@npm:1.2.11": version: 1.2.11 resolution: "web3-shh@npm:1.2.11" @@ -17516,6 +17718,21 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-utils@npm:1.8.0": + version: 1.8.0 + resolution: "web3-utils@npm:1.8.0" + dependencies: + bn.js: ^5.2.1 + ethereum-bloom-filters: ^1.0.6 + ethereumjs-util: ^7.1.0 + ethjs-unit: 0.1.6 + number-to-bn: 1.7.0 + randombytes: ^2.1.0 + utf8: 3.0.0 + checksum: 9ac6b8be14fd1feb8f6744d97f94a043716f360035f07de5a6ab414b25838922ac06e09141af39f66fbf3e76de5f1c3e07807929adf754a28ac2159e32dacedf + languageName: node + linkType: hard + "web3-utils@npm:^1.0.0-beta.31, web3-utils@npm:^1.3.0": version: 1.6.0 resolution: "web3-utils@npm:1.6.0" From 42ce936a639e1aee4dde903a43ad596f5bc03744 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Wed, 12 Oct 2022 06:39:18 -0400 Subject: [PATCH 16/30] feat(gsn): gsn contracts --- contracts/mocks/contracts/TestHub.sol | 55 ++++ contracts/mocks/contracts/TestToken.sol | 14 + contracts/mocks/utils/AllEvents.sol | 32 +++ contracts/mocks/utils/RelayRegistrar.sol | 143 ++++++++++ contracts/mocks/utils/StakeManager.sol | 275 +++++++++++++++++++ contracts/mocks/utils/TokenGasCalculator.sol | 61 ++++ 6 files changed, 580 insertions(+) create mode 100644 contracts/mocks/contracts/TestHub.sol create mode 100644 contracts/mocks/contracts/TestToken.sol create mode 100644 contracts/mocks/utils/AllEvents.sol create mode 100644 contracts/mocks/utils/RelayRegistrar.sol create mode 100644 contracts/mocks/utils/StakeManager.sol create mode 100644 contracts/mocks/utils/TokenGasCalculator.sol diff --git a/contracts/mocks/contracts/TestHub.sol b/contracts/mocks/contracts/TestHub.sol new file mode 100644 index 000000000..bc26a2343 --- /dev/null +++ b/contracts/mocks/contracts/TestHub.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "../../protocol/meta-tx/libraries/GsnTypes.sol"; +import "../../protocol/meta-tx/interfaces/IPaymaster.sol"; + +import "../../protocol/meta-tx/RelayHub.sol"; + +import "../utils/AllEvents.sol"; + +/** + * This mock relay hub contract is only used to test the paymaster's 'pre-' and 'postRelayedCall' in isolation. + */ +contract TestHub is RelayHub, AllEvents { + constructor( + IStakeManager _stakeManager, + address _penalizer, + address _batchGateway, + address _relayRegistrar, + RelayHubConfig memory _config + ) + RelayHub(_stakeManager, _penalizer, _batchGateway, _relayRegistrar, _config) + // solhint-disable-next-line no-empty-blocks + { + + } + + function callPreRC( + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData, + uint256 maxPossibleGas + ) external returns (bytes memory context, bool revertOnRecipientRevert) { + IPaymaster paymaster = IPaymaster(relayRequest.relayData.paymaster); + IPaymaster.GasAndDataLimits memory limits = paymaster.getGasAndDataLimits(); + return + paymaster.preRelayedCall{ gas: limits.preRelayedCallGasLimit }( + relayRequest, + signature, + approvalData, + maxPossibleGas + ); + } + + function callPostRC( + IPaymaster paymaster, + bytes calldata context, + uint256 gasUseWithoutPost, + GsnTypes.RelayData calldata relayData + ) external { + IPaymaster.GasAndDataLimits memory limits = paymaster.getGasAndDataLimits(); + paymaster.postRelayedCall{ gas: limits.postRelayedCallGasLimit }(context, true, gasUseWithoutPost, relayData); + } +} diff --git a/contracts/mocks/contracts/TestToken.sol b/contracts/mocks/contracts/TestToken.sol new file mode 100644 index 000000000..bd65d751b --- /dev/null +++ b/contracts/mocks/contracts/TestToken.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestToken is ERC20 { + /* solhint-disable no-empty-blocks */ + constructor(string memory name, string memory symbol) public ERC20(name, symbol) {} + + function mint(address account, uint256 amount) public { + _mint(account, amount); + } +} diff --git a/contracts/mocks/utils/AllEvents.sol b/contracts/mocks/utils/AllEvents.sol new file mode 100644 index 000000000..33e0f97f8 --- /dev/null +++ b/contracts/mocks/utils/AllEvents.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.8.7; + +/** + * In order to help the Truffle tests to decode events in the transactions' results, + * the events must be declared in a top-level contract. + * Implement this empty interface in order to add event signatures to any contract. + * + */ +interface AllEvents { + event Received(address indexed sender, uint256 eth); + event Withdrawal(address indexed src, uint256 wad); + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + event TokensCharged( + uint256 gasUseWithoutPost, + uint256 gasJustPost, + uint256 tokenActualCharge, + uint256 ethActualCharge + ); + event UniswapReverted(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin); + + event Swap( + address indexed sender, + address indexed recipient, + int256 amount0, + int256 amount1, + uint160 sqrtPriceX96, + uint128 liquidity, + int24 tick + ); +} diff --git a/contracts/mocks/utils/RelayRegistrar.sol b/contracts/mocks/utils/RelayRegistrar.sol new file mode 100644 index 000000000..8abd2fcaa --- /dev/null +++ b/contracts/mocks/utils/RelayRegistrar.sol @@ -0,0 +1,143 @@ +// solhint-disable not-rely-on-time +//SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.6; +/* solhint-disable no-inline-assembly */ + +// #if ENABLE_CONSOLE_LOG +import "hardhat/console.sol"; +// #endif + +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; + +import "../../protocol/meta-tx/libraries/MinLibBytes.sol"; +import "../../protocol/meta-tx/interfaces/IRelayHub.sol"; +import "../../protocol/meta-tx/interfaces/IRelayRegistrar.sol"; + +/** + * @title The RelayRegistrar Implementation + * @notice Keeps a list of registered relayers. + * + * @notice Provides view functions to read the list of registered relayers and filters out invalid ones. + * + * @notice Protects the list from spamming entries: only staked relayers are added. + */ +contract RelayRegistrar is IRelayRegistrar, Ownable, ERC165 { + using MinLibBytes for bytes; + + uint256 private constant MAX_RELAYS_RETURNED_COUNT = 1000; + + /// @notice Mapping from `RelayHub` address to a mapping from a Relay Manager address to its registration details. + mapping(address => mapping(address => RelayInfo)) internal values; + + /// @notice Mapping from `RelayHub` address to an array of Relay Managers that are registered on that `RelayHub`. + mapping(address => address[]) internal indexedValues; + + uint256 private immutable creationBlock; + + uint256 private relayRegistrationMaxAge; + + constructor(uint256 _relayRegistrationMaxAge) { + setRelayRegistrationMaxAge(_relayRegistrationMaxAge); + creationBlock = block.number; + } + + /// @inheritdoc IRelayRegistrar + function getCreationBlock() external view override returns (uint256) { + return creationBlock; + } + + /// @inheritdoc IRelayRegistrar + function getRelayRegistrationMaxAge() external view override returns (uint256) { + return relayRegistrationMaxAge; + } + + /// @inheritdoc IRelayRegistrar + function setRelayRegistrationMaxAge(uint256 _relayRegistrationMaxAge) public override onlyOwner { + relayRegistrationMaxAge = _relayRegistrationMaxAge; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(IRelayRegistrar).interfaceId || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IRelayRegistrar + function registerRelayServer(address relayHub, bytes32[3] calldata url) external override { + address relayManager = msg.sender; + IRelayHub(relayHub).onRelayServerRegistered(relayManager); + emit RelayServerRegistered(relayManager, relayHub, url); + storeRelayServerRegistration(relayManager, relayHub, url); + } + + function addItem(address relayHub, address relayManager) internal returns (RelayInfo storage) { + RelayInfo storage storageInfo = values[relayHub][relayManager]; + if (storageInfo.lastSeenBlockNumber == 0) { + indexedValues[relayHub].push(relayManager); + } + return storageInfo; + } + + function storeRelayServerRegistration( + address relayManager, + address relayHub, + bytes32[3] calldata url + ) internal { + RelayInfo storage storageInfo = addItem(relayHub, relayManager); + if (storageInfo.firstSeenBlockNumber == 0) { + storageInfo.firstSeenBlockNumber = uint32(block.number); + storageInfo.firstSeenTimestamp = uint40(block.timestamp); + } + storageInfo.lastSeenBlockNumber = uint32(block.number); + storageInfo.lastSeenTimestamp = uint40(block.timestamp); + storageInfo.relayManager = relayManager; + storageInfo.urlParts = url; + } + + /// @inheritdoc IRelayRegistrar + function getRelayInfo(address relayHub, address relayManager) public view override returns (RelayInfo memory) { + RelayInfo memory info = values[relayHub][relayManager]; + require(info.lastSeenBlockNumber != 0, "relayManager not found"); + return info; + } + + /// @inheritdoc IRelayRegistrar + function readRelayInfos(address relayHub) public view override returns (RelayInfo[] memory info) { + uint256 blockTimestamp = block.timestamp; + uint256 oldestBlockTimestamp = + blockTimestamp >= relayRegistrationMaxAge ? blockTimestamp - relayRegistrationMaxAge : 0; + return readRelayInfosInRange(relayHub, 0, oldestBlockTimestamp, MAX_RELAYS_RETURNED_COUNT); + } + + /// @inheritdoc IRelayRegistrar + function readRelayInfosInRange( + address relayHub, + uint256 oldestBlockNumber, + uint256 oldestBlockTimestamp, + uint256 maxCount + ) public view override returns (RelayInfo[] memory info) { + address[] storage items = indexedValues[relayHub]; + uint256 filled = 0; + info = new RelayInfo[](items.length < maxCount ? items.length : maxCount); + for (uint256 i = 0; i < items.length; i++) { + address relayManager = items[i]; + RelayInfo memory relayInfo = getRelayInfo(relayHub, relayManager); + if ( + relayInfo.lastSeenBlockNumber < oldestBlockNumber || relayInfo.lastSeenTimestamp < oldestBlockTimestamp + ) { + continue; + } + // solhint-disable-next-line no-empty-blocks + try IRelayHub(relayHub).verifyRelayManagerStaked(relayManager) {} catch ( + bytes memory /*lowLevelData*/ + ) { + continue; + } + info[filled++] = relayInfo; + if (filled >= maxCount) break; + } + assembly { + mstore(info, filled) + } + } +} diff --git a/contracts/mocks/utils/StakeManager.sol b/contracts/mocks/utils/StakeManager.sol new file mode 100644 index 000000000..a74a2a60a --- /dev/null +++ b/contracts/mocks/utils/StakeManager.sol @@ -0,0 +1,275 @@ +// solhint-disable not-rely-on-time +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; +import "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; + +import "../../protocol/meta-tx/interfaces/IStakeManager.sol"; + +/** + * @title The StakeManager implementation + * @notice An IStakeManager instance that accepts stakes in any ERC-20 token. + * + * @notice Single StakeInfo of a single RelayManager can only have one token address assigned to it. + * + * @notice It cannot be changed after the first time 'stakeForRelayManager' is called as it is equivalent to withdrawal. + */ +contract StakeManager is IStakeManager, Ownable, ERC165 { + using SafeERC20 for IERC20; + + string public override versionSM = "3.0.0-beta.2+opengsn.stakemanager.istakemanager"; + uint256 internal immutable maxUnstakeDelay; + + AbandonedRelayServerConfig internal abandonedRelayServerConfig; + + address internal burnAddress; + uint256 internal immutable creationBlock; + + /// maps relay managers to their stakes + mapping(address => StakeInfo) public stakes; + + /// @inheritdoc IStakeManager + function getStakeInfo(address relayManager) + external + view + override + returns (StakeInfo memory stakeInfo, bool isSenderAuthorizedHub) + { + bool isHubAuthorized = authorizedHubs[relayManager][msg.sender].removalTime == type(uint256).max; + return (stakes[relayManager], isHubAuthorized); + } + + /// @inheritdoc IStakeManager + function setBurnAddress(address _burnAddress) public override onlyOwner { + burnAddress = _burnAddress; + emit BurnAddressSet(burnAddress); + } + + /// @inheritdoc IStakeManager + function getBurnAddress() external view override returns (address) { + return burnAddress; + } + + /// @inheritdoc IStakeManager + function setDevAddress(address _devAddress) public override onlyOwner { + abandonedRelayServerConfig.devAddress = _devAddress; + emit DevAddressSet(abandonedRelayServerConfig.devAddress); + } + + /// @inheritdoc IStakeManager + function getAbandonedRelayServerConfig() external view override returns (AbandonedRelayServerConfig memory) { + return abandonedRelayServerConfig; + } + + /// @inheritdoc IStakeManager + function getMaxUnstakeDelay() external view override returns (uint256) { + return maxUnstakeDelay; + } + + /// maps relay managers to a map of addressed of their authorized hubs to the information on that hub + mapping(address => mapping(address => RelayHubInfo)) public authorizedHubs; + + constructor( + uint256 _maxUnstakeDelay, + uint256 _abandonmentDelay, + uint256 _escheatmentDelay, + address _burnAddress, + address _devAddress + ) { + require(_burnAddress != address(0), "transfers to address(0) may fail"); + setBurnAddress(_burnAddress); + setDevAddress(_devAddress); + creationBlock = block.number; + maxUnstakeDelay = _maxUnstakeDelay; + abandonedRelayServerConfig.abandonmentDelay = _abandonmentDelay; + abandonedRelayServerConfig.escheatmentDelay = _escheatmentDelay; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(IStakeManager).interfaceId || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IStakeManager + function getCreationBlock() external view override returns (uint256) { + return creationBlock; + } + + /// @inheritdoc IStakeManager + function setRelayManagerOwner(address owner) external override { + require(owner != address(0), "invalid owner"); + require(stakes[msg.sender].owner == address(0), "already owned"); + stakes[msg.sender].owner = owner; + emit OwnerSet(msg.sender, owner); + } + + /// @inheritdoc IStakeManager + function stakeForRelayManager( + IERC20 token, + address relayManager, + uint256 unstakeDelay, + uint256 amount + ) external override relayOwnerOnly(relayManager) { + require(unstakeDelay >= stakes[relayManager].unstakeDelay, "unstakeDelay cannot be decreased"); + require(unstakeDelay <= maxUnstakeDelay, "unstakeDelay too big"); + require(token != IERC20(address(0)), "must specify stake token address"); + require( + stakes[relayManager].token == IERC20(address(0)) || stakes[relayManager].token == token, + "stake token address is incorrect" + ); + token.safeTransferFrom(msg.sender, address(this), amount); + stakes[relayManager].token = token; + stakes[relayManager].stake += amount; + stakes[relayManager].unstakeDelay = unstakeDelay; + emit StakeAdded( + relayManager, + stakes[relayManager].owner, + stakes[relayManager].token, + stakes[relayManager].stake, + stakes[relayManager].unstakeDelay + ); + } + + /// @inheritdoc IStakeManager + function unlockStake(address relayManager) external override relayOwnerOnly(relayManager) { + StakeInfo storage info = stakes[relayManager]; + require(info.withdrawTime == 0, "already pending"); + info.withdrawTime = block.timestamp + info.unstakeDelay; + emit StakeUnlocked(relayManager, msg.sender, info.withdrawTime); + } + + /// @inheritdoc IStakeManager + function withdrawStake(address relayManager) external override relayOwnerOnly(relayManager) { + StakeInfo storage info = stakes[relayManager]; + require(info.withdrawTime > 0, "Withdrawal is not scheduled"); + require(info.withdrawTime <= block.timestamp, "Withdrawal is not due"); + uint256 amount = info.stake; + info.stake = 0; + info.withdrawTime = 0; + info.token.safeTransfer(msg.sender, amount); + emit StakeWithdrawn(relayManager, msg.sender, info.token, amount); + } + + /// @notice Prevents any address other than a registered Relay Owner from calling this method. + modifier relayOwnerOnly(address relayManager) { + StakeInfo storage info = stakes[relayManager]; + require(info.owner == msg.sender, "not owner"); + _; + } + + /// @notice Prevents any address other than a registered Relay Manager from calling this method. + modifier managerOnly() { + StakeInfo storage info = stakes[msg.sender]; + require(info.owner != address(0), "not manager"); + _; + } + + /// @inheritdoc IStakeManager + function authorizeHubByOwner(address relayManager, address relayHub) + external + override + relayOwnerOnly(relayManager) + { + _authorizeHub(relayManager, relayHub); + } + + /// @inheritdoc IStakeManager + function authorizeHubByManager(address relayHub) external override managerOnly { + _authorizeHub(msg.sender, relayHub); + } + + function _authorizeHub(address relayManager, address relayHub) internal { + authorizedHubs[relayManager][relayHub].removalTime = type(uint256).max; + emit HubAuthorized(relayManager, relayHub); + } + + /// @inheritdoc IStakeManager + function unauthorizeHubByOwner(address relayManager, address relayHub) + external + override + relayOwnerOnly(relayManager) + { + _unauthorizeHub(relayManager, relayHub); + } + + /// @inheritdoc IStakeManager + function unauthorizeHubByManager(address relayHub) external override managerOnly { + _unauthorizeHub(msg.sender, relayHub); + } + + function _unauthorizeHub(address relayManager, address relayHub) internal { + RelayHubInfo storage hubInfo = authorizedHubs[relayManager][relayHub]; + require(hubInfo.removalTime == type(uint256).max, "hub not authorized"); + hubInfo.removalTime = block.timestamp + stakes[relayManager].unstakeDelay; + emit HubUnauthorized(relayManager, relayHub, hubInfo.removalTime); + } + + /// @inheritdoc IStakeManager + function penalizeRelayManager( + address relayManager, + address beneficiary, + uint256 amount + ) external override { + uint256 removalTime = authorizedHubs[relayManager][msg.sender].removalTime; + require(removalTime != 0, "hub not authorized"); + require(removalTime > block.timestamp, "hub authorization expired"); + + // Half of the stake will be burned (sent to address 0) + require(stakes[relayManager].stake >= amount, "penalty exceeds stake"); + stakes[relayManager].stake = stakes[relayManager].stake - amount; + + uint256 toBurn = amount / 2; + uint256 reward = amount - toBurn; + + // Stake ERC-20 token is burned and transferred + stakes[relayManager].token.safeTransfer(burnAddress, toBurn); + stakes[relayManager].token.safeTransfer(beneficiary, reward); + emit StakePenalized(relayManager, beneficiary, stakes[relayManager].token, reward); + } + + /// @inheritdoc IStakeManager + function isRelayEscheatable(address relayManager) public view override returns (bool) { + IStakeManager.StakeInfo memory stakeInfo = stakes[relayManager]; + return + stakeInfo.abandonedTime != 0 && + stakeInfo.abandonedTime + abandonedRelayServerConfig.escheatmentDelay < block.timestamp; + } + + /// @inheritdoc IStakeManager + function markRelayAbandoned(address relayManager) external override onlyOwner { + StakeInfo storage info = stakes[relayManager]; + require(info.stake > 0, "relay manager not staked"); + require(info.abandonedTime == 0, "relay manager already abandoned"); + require( + info.keepaliveTime + abandonedRelayServerConfig.abandonmentDelay < block.timestamp, + "relay manager was alive recently" + ); + info.abandonedTime = block.timestamp; + emit RelayServerAbandoned(relayManager, info.abandonedTime); + } + + /// @inheritdoc IStakeManager + function escheatAbandonedRelayStake(address relayManager) external override onlyOwner { + StakeInfo storage info = stakes[relayManager]; + require(isRelayEscheatable(relayManager), "relay server not escheatable yet"); + uint256 amount = info.stake; + info.stake = 0; + info.withdrawTime = 0; + info.token.safeTransfer(abandonedRelayServerConfig.devAddress, amount); + emit AbandonedRelayManagerStakeEscheated(relayManager, msg.sender, info.token, amount); + } + + /// @inheritdoc IStakeManager + function updateRelayKeepaliveTime(address relayManager) external override { + StakeInfo storage info = stakes[relayManager]; + bool isHubAuthorized = authorizedHubs[relayManager][msg.sender].removalTime == type(uint256).max; + bool isRelayOwner = info.owner == msg.sender; + require(isHubAuthorized || isRelayOwner, "must be called by owner or hub"); + info.abandonedTime = 0; + info.keepaliveTime = block.timestamp; + emit RelayServerKeepalive(relayManager, info.keepaliveTime); + } +} diff --git a/contracts/mocks/utils/TokenGasCalculator.sol b/contracts/mocks/utils/TokenGasCalculator.sol new file mode 100644 index 000000000..a9eced211 --- /dev/null +++ b/contracts/mocks/utils/TokenGasCalculator.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; + +import "../../protocol/meta-tx/RelayHub.sol"; +import "../../protocol/meta-tx/BasePaymaster.sol"; +import "./AllEvents.sol"; + +/** + * Calculate the postRelayedCall gas usage for a TokenPaymaster. + * + */ +contract TokenGasCalculator is RelayHub, AllEvents { + //(The Paymaster calls back calculateCharge, depositFor in the relayHub, + //so the calculator has to implement them just like a real RelayHub + // solhint-disable-next-line no-empty-blocks + constructor( + IStakeManager _stakeManager, + address _penalizer, + address _batchGateway, + address _relayRegistrar, + RelayHubConfig memory _config + ) + RelayHub(_stakeManager, _penalizer, _batchGateway, _relayRegistrar, _config) + // solhint-disable-next-line no-empty-blocks + { + + } + + /** + * calculate actual cost of postRelayedCall. + * usage: + * - create this calculator. + * - create an instance of your TokenPaymaster, with your token's Uniswap instance. + * - move some tokens (1000 "wei") to the calculator (msg.sender is given approval to pull them back at the end) + * - set the calculator as owner of this calculator. + * - call this method. + * - use the returned values to set your real TokenPaymaster.setPostGasUsage() + * the above can be ran on a "forked" network, so that it will have the real token, uniswap instances, + * but still leave no side-effect on the network. + */ + function calculatePostGas( + BasePaymaster paymaster, + bytes memory ctx1, + bytes memory paymasterData + ) public returns (uint256 gasUsedByPost) { + GsnTypes.RelayData memory relayData = + GsnTypes.RelayData(1, 1, 0, address(0), address(0), address(0), paymasterData, 0); + + //with precharge + uint256 gas0 = gasleft(); + paymaster.postRelayedCall(ctx1, true, 1000000000000000, relayData); + uint256 gas1 = gasleft(); + gasUsedByPost = gas0 - gas1; + emit GasUsed(gasUsedByPost); + } + + event GasUsed(uint256 gasUsedByPost); +} From 342d71ed0df822a51fc7c5524bc1846fef6e132c Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Wed, 12 Oct 2022 06:40:42 -0400 Subject: [PATCH 17/30] feat(gsn): gsn contracts --- .../meta-tx/{ => GSN}/BasePaymaster.sol | 7 + contracts/protocol/meta-tx/GSN/Penalizer.sol | 183 +++++ contracts/protocol/meta-tx/GSN/RelayHub.sol | 677 ++++++++++++++++++ .../{ => GSN}/interfaces/IGsnRelayHub.sol | 0 .../{ => GSN}/interfaces/IPaymaster.sol | 0 .../{ => GSN}/interfaces/IPenalizer.sol | 0 .../{ => GSN}/interfaces/IRelayHub.sol | 0 .../{ => GSN}/interfaces/IRelayRegistrar.sol | 0 .../{ => GSN}/interfaces/IStakeManager.sol | 0 .../{ => GSN}/libraries/GsnEip712Library.sol | 2 + .../meta-tx/{ => GSN}/libraries/GsnTypes.sol | 0 .../meta-tx/{ => GSN}/libraries/GsnUtils.sol | 0 .../{ => GSN}/libraries/MinLibBytes.sol | 0 .../meta-tx/GSN/libraries/RLPReader.sol | 266 +++++++ .../GSN/libraries/RelayHubValidator.sol | 49 ++ 15 files changed, 1184 insertions(+) rename contracts/protocol/meta-tx/{ => GSN}/BasePaymaster.sol (96%) create mode 100644 contracts/protocol/meta-tx/GSN/Penalizer.sol create mode 100644 contracts/protocol/meta-tx/GSN/RelayHub.sol rename contracts/protocol/meta-tx/{ => GSN}/interfaces/IGsnRelayHub.sol (100%) rename contracts/protocol/meta-tx/{ => GSN}/interfaces/IPaymaster.sol (100%) rename contracts/protocol/meta-tx/{ => GSN}/interfaces/IPenalizer.sol (100%) rename contracts/protocol/meta-tx/{ => GSN}/interfaces/IRelayHub.sol (100%) rename contracts/protocol/meta-tx/{ => GSN}/interfaces/IRelayRegistrar.sol (100%) rename contracts/protocol/meta-tx/{ => GSN}/interfaces/IStakeManager.sol (100%) rename contracts/protocol/meta-tx/{ => GSN}/libraries/GsnEip712Library.sol (99%) rename contracts/protocol/meta-tx/{ => GSN}/libraries/GsnTypes.sol (100%) rename contracts/protocol/meta-tx/{ => GSN}/libraries/GsnUtils.sol (100%) rename contracts/protocol/meta-tx/{ => GSN}/libraries/MinLibBytes.sol (100%) create mode 100644 contracts/protocol/meta-tx/GSN/libraries/RLPReader.sol create mode 100644 contracts/protocol/meta-tx/GSN/libraries/RelayHubValidator.sol diff --git a/contracts/protocol/meta-tx/BasePaymaster.sol b/contracts/protocol/meta-tx/GSN/BasePaymaster.sol similarity index 96% rename from contracts/protocol/meta-tx/BasePaymaster.sol rename to contracts/protocol/meta-tx/GSN/BasePaymaster.sol index 228bc17bf..ceb82e714 100644 --- a/contracts/protocol/meta-tx/BasePaymaster.sol +++ b/contracts/protocol/meta-tx/GSN/BasePaymaster.sol @@ -11,6 +11,7 @@ import "./interfaces/IPaymaster.sol"; import "./interfaces/IRelayHub.sol"; import "./libraries/GsnEip712Library.sol"; import "./interfaces/IForwarder.sol"; +import "hardhat/console.sol"; /** * @notice An abstract base class to be inherited by a concrete Paymaster. @@ -129,11 +130,17 @@ abstract contract BasePaymaster is IPaymaster, Ownable, ERC165 { bytes calldata approvalData, uint256 maxPossibleGas ) external override returns (bytes memory, bool) { + console.log("_verifyRelayHubOnly"); _verifyRelayHubOnly(); + console.log("_verifyForwarder"); _verifyForwarder(relayRequest); + console.log("_verifyValue"); _verifyValue(relayRequest); + console.log("_verifyPaymasterData"); _verifyPaymasterData(relayRequest); + console.log("_verifyApprovalData"); _verifyApprovalData(approvalData); + console.log("_preRelayedCall"); return _preRelayedCall(relayRequest, signature, approvalData, maxPossibleGas); } diff --git a/contracts/protocol/meta-tx/GSN/Penalizer.sol b/contracts/protocol/meta-tx/GSN/Penalizer.sol new file mode 100644 index 000000000..025bac980 --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/Penalizer.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; + +import "./libraries/RLPReader.sol"; +import "./libraries/GsnUtils.sol"; +import "./interfaces/IRelayHub.sol"; +import "./interfaces/IPenalizer.sol"; + +/** + * @title The Penalizer Implementation + * + * @notice This Penalizer supports parsing Legacy, Type 1 and Type 2 raw RLP Encoded transactions. + */ +contract Penalizer is IPenalizer, ERC165 { + using ECDSA for bytes32; + + /// @inheritdoc IPenalizer + string public override versionPenalizer = "3.0.0-beta.2+opengsn.penalizer.ipenalizer"; + + uint256 internal immutable penalizeBlockDelay; + uint256 internal immutable penalizeBlockExpiration; + + constructor(uint256 _penalizeBlockDelay, uint256 _penalizeBlockExpiration) { + penalizeBlockDelay = _penalizeBlockDelay; + penalizeBlockExpiration = _penalizeBlockExpiration; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(IPenalizer).interfaceId || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IPenalizer + function getPenalizeBlockDelay() external view override returns (uint256) { + return penalizeBlockDelay; + } + + /// @inheritdoc IPenalizer + function getPenalizeBlockExpiration() external view override returns (uint256) { + return penalizeBlockExpiration; + } + + function isLegacyTransaction(bytes calldata rawTransaction) internal pure returns (bool) { + uint8 transactionTypeByte = uint8(rawTransaction[0]); + return (transactionTypeByte >= 0xc0 && transactionTypeByte <= 0xfe); + } + + function isTransactionType1(bytes calldata rawTransaction) internal pure returns (bool) { + return (uint8(rawTransaction[0]) == 1); + } + + function isTransactionType2(bytes calldata rawTransaction) internal pure returns (bool) { + return (uint8(rawTransaction[0]) == 2); + } + + /// @return `true` if raw transaction is of types Legacy, 1 or 2. `false` otherwise. + function isTransactionTypeValid(bytes calldata rawTransaction) public pure returns (bool) { + return + isLegacyTransaction(rawTransaction) || + isTransactionType1(rawTransaction) || + isTransactionType2(rawTransaction); + } + + /// @return transaction The details that the `Penalizer` needs to decide if the transaction is penalizable. + function decodeTransaction(bytes calldata rawTransaction) public pure returns (Transaction memory transaction) { + if (isTransactionType1(rawTransaction)) { + (transaction.nonce, transaction.gasLimit, transaction.to, transaction.value, transaction.data) = RLPReader + .decodeTransactionType1(rawTransaction); + } else if (isTransactionType2(rawTransaction)) { + (transaction.nonce, transaction.gasLimit, transaction.to, transaction.value, transaction.data) = RLPReader + .decodeTransactionType2(rawTransaction); + } else { + (transaction.nonce, transaction.gasLimit, transaction.to, transaction.value, transaction.data) = RLPReader + .decodeLegacyTransaction(rawTransaction); + } + return transaction; + } + + mapping(bytes32 => uint256) public commits; + + /// @inheritdoc IPenalizer + function commit(bytes32 commitHash) external override { + uint256 readyBlockNumber = block.number + penalizeBlockDelay; + commits[commitHash] = readyBlockNumber; + emit CommitAdded(msg.sender, commitHash, readyBlockNumber); + } + + /// Modifier that verifies there was a `commit` operation before this call that has not expired yet. + modifier commitRevealOnly() { + bytes32 commitHash = keccak256(abi.encodePacked(keccak256(msg.data), msg.sender)); + uint256 readyBlockNumber = commits[commitHash]; + delete commits[commitHash]; + // msg.sender can only be fake during off-chain view call, allowing Penalizer process to check transactions + if (msg.sender != address(type(uint160).max)) { + require(readyBlockNumber != 0, "no commit"); + require(readyBlockNumber < block.number, "reveal penalize too soon"); + require(readyBlockNumber + penalizeBlockExpiration > block.number, "reveal penalize too late"); + } + _; + } + + /// @inheritdoc IPenalizer + function penalizeRepeatedNonce( + bytes calldata unsignedTx1, + bytes calldata signature1, + bytes calldata unsignedTx2, + bytes calldata signature2, + IRelayHub hub, + uint256 randomValue + ) public override commitRevealOnly { + (randomValue); + _penalizeRepeatedNonce(unsignedTx1, signature1, unsignedTx2, signature2, hub); + } + + function _penalizeRepeatedNonce( + bytes calldata unsignedTx1, + bytes calldata signature1, + bytes calldata unsignedTx2, + bytes calldata signature2, + IRelayHub hub + ) private { + address addr1 = keccak256(unsignedTx1).recover(signature1); + address addr2 = keccak256(unsignedTx2).recover(signature2); + + require(addr1 == addr2, "Different signer"); + require(addr1 != address(0), "ecrecover failed"); + + Transaction memory decodedTx1 = decodeTransaction(unsignedTx1); + Transaction memory decodedTx2 = decodeTransaction(unsignedTx2); + + // checking that the same nonce is used in both transaction, with both signed by the same address + // and the actual data is different + // note: we compare the hash of the tx to save gas over iterating both byte arrays + require(decodedTx1.nonce == decodedTx2.nonce, "Different nonce"); + + bytes memory dataToCheck1 = + abi.encodePacked(decodedTx1.data, decodedTx1.gasLimit, decodedTx1.to, decodedTx1.value); + + bytes memory dataToCheck2 = + abi.encodePacked(decodedTx2.data, decodedTx2.gasLimit, decodedTx2.to, decodedTx2.value); + + require(keccak256(dataToCheck1) != keccak256(dataToCheck2), "tx is equal"); + + penalize(addr1, hub); + } + + /// @inheritdoc IPenalizer + function penalizeIllegalTransaction( + bytes calldata unsignedTx, + bytes calldata signature, + IRelayHub hub, + uint256 randomValue + ) public override commitRevealOnly { + (randomValue); + _penalizeIllegalTransaction(unsignedTx, signature, hub); + } + + function _penalizeIllegalTransaction( + bytes calldata unsignedTx, + bytes calldata signature, + IRelayHub hub + ) private { + if (isTransactionTypeValid(unsignedTx)) { + Transaction memory decodedTx = decodeTransaction(unsignedTx); + if (decodedTx.to == address(hub)) { + bytes4 selector = GsnUtils.getMethodSig(decodedTx.data); + bool isWrongMethodCall = selector != IRelayHub.relayCall.selector; + require(isWrongMethodCall, "Legal relay transaction"); + } + } + address relay = keccak256(unsignedTx).recover(signature); + require(relay != address(0), "ecrecover failed"); + penalize(relay, hub); + } + + function penalize(address relayWorker, IRelayHub hub) private { + hub.penalize(relayWorker, payable(msg.sender)); + } +} diff --git a/contracts/protocol/meta-tx/GSN/RelayHub.sol b/contracts/protocol/meta-tx/GSN/RelayHub.sol new file mode 100644 index 000000000..026a1d680 --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/RelayHub.sol @@ -0,0 +1,677 @@ +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable not-rely-on-time */ +/* solhint-disable avoid-tx-origin */ +/* solhint-disable bracket-align */ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +// #if ENABLE_CONSOLE_LOG +import "hardhat/console.sol"; +// #endif + +import "./libraries/MinLibBytes.sol"; +import "@openzeppelin/contracts-0.8.x/utils/Address.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165Checker.sol"; +import "@openzeppelin/contracts-0.8.x/utils/math/Math.sol"; +import "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; +import "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; + +import "./libraries/GsnUtils.sol"; +import "./libraries/GsnEip712Library.sol"; +import "./libraries/RelayHubValidator.sol"; +import "./libraries/GsnTypes.sol"; +import "./interfaces/IRelayHub.sol"; +import "./interfaces/IPaymaster.sol"; +import "./interfaces/IForwarder.sol"; +import "./interfaces/IStakeManager.sol"; +import "./interfaces/IRelayRegistrar.sol"; +import "./interfaces/IStakeManager.sol"; + +/** + * @title The RelayHub Implementation + * @notice This contract implements the `IRelayHub` interface for the EVM-compatible networks. + */ +contract RelayHub is IRelayHub, Ownable, ERC165 { + using ERC165Checker for address; + using Address for address; + + address private constant DRY_RUN_ADDRESS = 0x0000000000000000000000000000000000000000; + + /// @inheritdoc IRelayHub + function versionHub() public pure virtual override returns (string memory) { + return "3.0.0-beta.2+opengsn.hub.irelayhub"; + } + + IStakeManager internal immutable stakeManager; + address internal immutable penalizer; + address internal immutable batchGateway; + address internal immutable relayRegistrar; + + RelayHubConfig internal config; + + /// @inheritdoc IRelayHub + function getConfiguration() public view override returns (RelayHubConfig memory) { + return config; + } + + /// @inheritdoc IRelayHub + function setConfiguration(RelayHubConfig memory _config) public override onlyOwner { + require(_config.devFee < 100, "dev fee too high"); + config = _config; + emit RelayHubConfigured(config); + } + + // maps ERC-20 token address to a minimum stake for it + mapping(IERC20 => uint256) internal minimumStakePerToken; + + /// @inheritdoc IRelayHub + function setMinimumStakes(IERC20[] memory token, uint256[] memory minimumStake) public override onlyOwner { + require(token.length == minimumStake.length, "setMinimumStakes: wrong length"); + for (uint256 i = 0; i < token.length; i++) { + minimumStakePerToken[token[i]] = minimumStake[i]; + emit StakingTokenDataChanged(address(token[i]), minimumStake[i]); + } + } + + // maps relay worker's address to its manager's address + mapping(address => address) internal workerToManager; + + // maps relay managers to the number of their workers + mapping(address => uint256) internal workerCount; + + mapping(address => uint256) internal balances; + + uint256 internal immutable creationBlock; + uint256 internal deprecationTime = type(uint256).max; + + constructor( + IStakeManager _stakeManager, + address _penalizer, + address _batchGateway, + address _relayRegistrar, + RelayHubConfig memory _config + ) { + creationBlock = block.number; + stakeManager = _stakeManager; + penalizer = _penalizer; + batchGateway = _batchGateway; + relayRegistrar = _relayRegistrar; + setConfiguration(_config); + } + + /// @inheritdoc IRelayHub + function getCreationBlock() external view virtual override returns (uint256) { + return creationBlock; + } + + /// @inheritdoc IRelayHub + function getDeprecationTime() external view override returns (uint256) { + return deprecationTime; + } + + /// @inheritdoc IRelayHub + function getStakeManager() external view override returns (IStakeManager) { + return stakeManager; + } + + /// @inheritdoc IRelayHub + function getPenalizer() external view override returns (address) { + return penalizer; + } + + /// @inheritdoc IRelayHub + function getBatchGateway() external view override returns (address) { + return batchGateway; + } + + /// @inheritdoc IRelayHub + function getRelayRegistrar() external view override returns (address) { + return relayRegistrar; + } + + /// @inheritdoc IRelayHub + function getMinimumStakePerToken(IERC20 token) external view override returns (uint256) { + return minimumStakePerToken[token]; + } + + /// @inheritdoc IRelayHub + function getWorkerManager(address worker) external view override returns (address) { + return workerToManager[worker]; + } + + /// @inheritdoc IRelayHub + function getWorkerCount(address manager) external view override returns (uint256) { + return workerCount[manager]; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return + interfaceId == type(IRelayHub).interfaceId || + interfaceId == type(Ownable).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @inheritdoc IRelayHub + function onRelayServerRegistered(address relayManager) external override { + require(msg.sender == relayRegistrar, "caller is not relay registrar"); + verifyRelayManagerStaked(relayManager); + require(workerCount[relayManager] > 0, "no relay workers"); + stakeManager.updateRelayKeepaliveTime(relayManager); + } + + /// @inheritdoc IRelayHub + function addRelayWorkers(address[] calldata newRelayWorkers) external override { + address relayManager = msg.sender; + uint256 newWorkerCount = workerCount[relayManager] + newRelayWorkers.length; + workerCount[relayManager] = newWorkerCount; + require(newWorkerCount <= config.maxWorkerCount, "too many workers"); + + verifyRelayManagerStaked(relayManager); + + for (uint256 i = 0; i < newRelayWorkers.length; i++) { + require(workerToManager[newRelayWorkers[i]] == address(0), "this worker has a manager"); + workerToManager[newRelayWorkers[i]] = relayManager; + } + + emit RelayWorkersAdded(relayManager, newRelayWorkers, newWorkerCount); + } + + /// @inheritdoc IRelayHub + function depositFor(address target) public payable virtual override { + require(target.supportsInterface(type(IPaymaster).interfaceId), "target is not a valid IPaymaster"); + uint256 amount = msg.value; + + balances[target] = balances[target] + amount; + + emit Deposited(target, msg.sender, amount); + } + + /// @inheritdoc IRelayHub + function balanceOf(address target) external view override returns (uint256) { + return balances[target]; + } + + /// @inheritdoc IRelayHub + function withdraw(address payable dest, uint256 amount) public override { + uint256[] memory amounts = new uint256[](1); + address payable[] memory destinations = new address payable[](1); + amounts[0] = amount; + destinations[0] = dest; + withdrawMultiple(destinations, amounts); + } + + /// @inheritdoc IRelayHub + function withdrawMultiple(address payable[] memory dest, uint256[] memory amount) public override { + address payable account = payable(msg.sender); + for (uint256 i = 0; i < amount.length; i++) { + // #if ENABLE_CONSOLE_LOG + console.log("withdrawMultiple %s %s %s", balances[account], dest[i], amount[i]); + // #endif + uint256 balance = balances[account]; + require(balance >= amount[i], "insufficient funds"); + balances[account] = balance - amount[i]; + (bool success, ) = dest[i].call{ value: amount[i] }(""); + require(success, "Transfer failed."); + emit Withdrawn(account, dest[i], amount[i]); + } + } + + function verifyGasAndDataLimits( + uint256 maxAcceptanceBudget, + GsnTypes.RelayRequest calldata relayRequest, + uint256 initialGasLeft + ) private view returns (IPaymaster.GasAndDataLimits memory gasAndDataLimits, uint256 maxPossibleGas) { + gasAndDataLimits = IPaymaster(relayRequest.relayData.paymaster).getGasAndDataLimits{ gas: 50000 }(); + require(msg.data.length <= gasAndDataLimits.calldataSizeLimit, "msg.data exceeded limit"); + + require(maxAcceptanceBudget >= gasAndDataLimits.acceptanceBudget, "acceptance budget too high"); + require( + gasAndDataLimits.acceptanceBudget >= gasAndDataLimits.preRelayedCallGasLimit, + "acceptance budget too low" + ); + + maxPossibleGas = relayRequest.relayData.transactionCalldataGasUsed + initialGasLeft; + + uint256 maxPossibleCharge = calculateCharge(maxPossibleGas, relayRequest.relayData); + + // We don't yet know how much gas will be used by the recipient, so we make sure there are enough funds to pay + // for the maximum possible charge. + require(maxPossibleCharge <= balances[relayRequest.relayData.paymaster], "Paymaster balance too low"); + } + + struct RelayCallData { + bool success; + bytes4 functionSelector; + uint256 initialGasLeft; + bytes recipientContext; + bytes relayedCallReturnValue; + IPaymaster.GasAndDataLimits gasAndDataLimits; + RelayCallStatus status; + uint256 innerGasUsed; + uint256 maxPossibleGas; + uint256 innerGasLimit; + uint256 gasBeforeInner; + uint256 gasUsed; + uint256 devCharge; + bytes retData; + address relayManager; + bytes32 relayRequestId; + uint256 tmpInitialGas; + bytes relayCallStatus; + } + + /// @inheritdoc IRelayHub + function relayCall( + string calldata domainSeparatorName, + uint256 maxAcceptanceBudget, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData + ) + external + override + returns ( + bool paymasterAccepted, + uint256 charge, + IRelayHub.RelayCallStatus status, + bytes memory returnValue + ) + { + RelayCallData memory vars; + vars.initialGasLeft = aggregateGasleft(); + vars.relayRequestId = GsnUtils.getRelayRequestID(relayRequest, signature); + + // #if ENABLE_CONSOLE_LOG + console.log("relayCall relayRequestId"); + console.logBytes32(vars.relayRequestId); + console.log("relayCall relayRequest.request.from", relayRequest.request.from); + console.log("relayCall relayRequest.request.to", relayRequest.request.to); + console.log("relayCall relayRequest.request.value", relayRequest.request.value); + console.log("relayCall relayRequest.request.gas", relayRequest.request.gas); + console.log("relayCall relayRequest.request.nonce", relayRequest.request.nonce); + console.log("relayCall relayRequest.request.validUntilTime", relayRequest.request.validUntilTime); + + console.log("relayCall relayRequest.relayData.maxFeePerGas", relayRequest.relayData.maxFeePerGas); + console.log( + "relayCall relayRequest.relayData.maxPriorityFeePerGas", + relayRequest.relayData.maxPriorityFeePerGas + ); + console.log( + "relayCall relayRequest.relayData.transactionCalldataGasUsed", + relayRequest.relayData.transactionCalldataGasUsed + ); + console.log("relayCall relayRequest.relayData.relayWorker", relayRequest.relayData.relayWorker); + console.log("relayCall relayRequest.relayData.paymaster", relayRequest.relayData.paymaster); + console.log("relayCall relayRequest.relayData.forwarder", relayRequest.relayData.forwarder); + console.log("relayCall relayRequest.relayData.clientId", relayRequest.relayData.clientId); + + console.log("relayCall domainSeparatorName"); + console.logString(domainSeparatorName); + console.log("relayCall signature"); + console.logBytes(signature); + console.log("relayCall approvalData"); + console.logBytes(approvalData); + console.log("relayCall relayRequest.request.data"); + console.logBytes(relayRequest.request.data); + console.log("relayCall relayRequest.relayData.paymasterData"); + console.logBytes(relayRequest.relayData.paymasterData); + console.log("relayCall maxAcceptanceBudget", maxAcceptanceBudget); + // #endif + + require(!isDeprecated(), "hub deprecated"); + vars.functionSelector = relayRequest.request.data.length >= 4 + ? MinLibBytes.readBytes4(relayRequest.request.data, 0) + : bytes4(0); + + if (msg.sender != batchGateway && tx.origin != DRY_RUN_ADDRESS) { + require(signature.length != 0, "missing signature or bad gateway"); + require(msg.sender == tx.origin, "relay worker must be EOA"); + require(msg.sender == relayRequest.relayData.relayWorker, "Not a right worker"); + } + + if (tx.origin != DRY_RUN_ADDRESS) { + vars.relayManager = workerToManager[relayRequest.relayData.relayWorker]; + require(vars.relayManager != address(0), "Unknown relay worker"); + verifyRelayManagerStaked(vars.relayManager); + } + + (vars.gasAndDataLimits, vars.maxPossibleGas) = verifyGasAndDataLimits( + maxAcceptanceBudget, + relayRequest, + vars.initialGasLeft + ); + + RelayHubValidator.verifyTransactionPacking(domainSeparatorName, relayRequest, signature, approvalData); + + { + //How much gas to pass down to innerRelayCall. must be lower than the default 63/64 + // actually, min(gasleft*63/64, gasleft-GAS_RESERVE) might be enough. + vars.innerGasLimit = (gasleft() * 63) / 64 - config.gasReserve; + vars.gasBeforeInner = aggregateGasleft(); + + /* + Preparing to calculate "gasUseWithoutPost": + MPG = calldataGasUsage + vars.initialGasLeft :: max possible gas, an approximate gas limit for the current transaction + GU1 = MPG - gasleft(called right before innerRelayCall) :: gas actually used by current transaction until that point + GU2 = innerGasLimit - gasleft(called inside the innerRelayCall just before preRelayedCall) :: gas actually used by innerRelayCall before calling postRelayCall + GWP1 = GU1 + GU2 :: gas actually used by the entire transaction before calling postRelayCall + TGO = config.gasOverhead + config.postOverhead :: extra that will be added to the charge to cover hidden costs + GWP = GWP1 + TGO :: transaction "gas used without postRelayCall" + */ + vars.tmpInitialGas = + relayRequest.relayData.transactionCalldataGasUsed + + vars.initialGasLeft + + vars.innerGasLimit + + config.gasOverhead + + config.postOverhead; + // Calls to the recipient are performed atomically inside an inner transaction which may revert in case of + // errors in the recipient. In either case (revert or regular execution) the return data encodes the + // RelayCallStatus value. + (vars.success, vars.relayCallStatus) = address(this).call{ gas: vars.innerGasLimit }( + abi.encodeWithSelector( + RelayHub.innerRelayCall.selector, + domainSeparatorName, + relayRequest, + signature, + approvalData, + vars.gasAndDataLimits, + vars.tmpInitialGas - aggregateGasleft(), /* totalInitialGas */ + vars.maxPossibleGas + ) + ); + vars.innerGasUsed = vars.gasBeforeInner - aggregateGasleft(); + (vars.status, vars.relayedCallReturnValue) = abi.decode(vars.relayCallStatus, (RelayCallStatus, bytes)); + if (vars.relayedCallReturnValue.length > 0) { + emit TransactionResult(vars.status, vars.relayedCallReturnValue); + } + } + { + if (!vars.success) { + //Failure cases where the PM doesn't pay + if ( + vars.status == RelayCallStatus.RejectedByPreRelayed || //can only be thrown if rejectOnRecipientRevert==true + ((vars.innerGasUsed <= + vars.gasAndDataLimits.acceptanceBudget + relayRequest.relayData.transactionCalldataGasUsed) && + (vars.status == RelayCallStatus.RejectedByForwarder || + vars.status == RelayCallStatus.RejectedByRecipientRevert)) + ) { + emit TransactionRejectedByPaymaster( + vars.relayManager, + relayRequest.relayData.paymaster, + vars.relayRequestId, + relayRequest.request.from, + relayRequest.request.to, + msg.sender, + vars.functionSelector, + vars.innerGasUsed, + vars.relayedCallReturnValue + ); + return (false, 0, vars.status, vars.relayedCallReturnValue); + } + } + + // We now perform the actual charge calculation, based on the measured gas used + vars.gasUsed = + relayRequest.relayData.transactionCalldataGasUsed + + (vars.initialGasLeft - aggregateGasleft()) + + config.gasOverhead; + charge = calculateCharge(vars.gasUsed, relayRequest.relayData); + vars.devCharge = calculateDevCharge(charge); + + balances[relayRequest.relayData.paymaster] = balances[relayRequest.relayData.paymaster] - charge; + balances[vars.relayManager] = balances[vars.relayManager] + (charge - vars.devCharge); + if (vars.devCharge > 0) { + // save some gas in case of zero dev charge + balances[config.devAddress] = balances[config.devAddress] + vars.devCharge; + } + + { + address from = relayRequest.request.from; + address to = relayRequest.request.to; + address paymaster = relayRequest.relayData.paymaster; + emit TransactionRelayed( + vars.relayManager, + msg.sender, + vars.relayRequestId, + from, + to, + paymaster, + vars.functionSelector, + vars.status, + charge + ); + } + + // avoid variable size memory copying after gas calculation completed on-chain + if (tx.origin == DRY_RUN_ADDRESS) { + return (true, charge, vars.status, vars.relayedCallReturnValue); + } + return (true, charge, vars.status, ""); + } + } + + struct InnerRelayCallData { + uint256 initialGasLeft; + uint256 gasUsedToCallInner; + uint256 balanceBefore; + bytes32 preReturnValue; + bool relayedCallSuccess; + bytes relayedCallReturnValue; + bytes recipientContext; + bytes data; + bool rejectOnRecipientRevert; + } + + /** + * @notice This method can only by called by this `RelayHub`. + * It wraps the execution of the `RelayRequest` in a revertable frame context. + */ + function innerRelayCall( + string calldata domainSeparatorName, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData, + IPaymaster.GasAndDataLimits calldata gasAndDataLimits, + uint256 totalInitialGas, + uint256 maxPossibleGas + ) external returns (RelayCallStatus, bytes memory) { + InnerRelayCallData memory vars; + vars.initialGasLeft = aggregateGasleft(); + vars.gasUsedToCallInner = totalInitialGas - gasleft(); + // A new gas measurement is performed inside innerRelayCall, since + // due to EIP150 available gas amounts cannot be directly compared across external calls + + // This external function can only be called by RelayHub itself, creating an internal transaction. Calls to the + // recipient (preRelayedCall, the relayedCall, and postRelayedCall) are called from inside this transaction. + require(msg.sender == address(this), "Must be called by RelayHub"); + + // If either pre or post reverts, the whole internal transaction will be reverted, reverting all side effects on + // the recipient. The recipient will still be charged for the used gas by the relay. + + // The paymaster is no allowed to withdraw balance from RelayHub during a relayed transaction. We check pre and + // post state to ensure this doesn't happen. + vars.balanceBefore = balances[relayRequest.relayData.paymaster]; + + // First preRelayedCall is executed. + // Note: we open a new block to avoid growing the stack too much. + vars.data = abi.encodeWithSelector( + IPaymaster.preRelayedCall.selector, + relayRequest, + signature, + approvalData, + maxPossibleGas + ); + { + bool success; + bytes memory retData; + (success, retData) = relayRequest.relayData.paymaster.call{ gas: gasAndDataLimits.preRelayedCallGasLimit }( + vars.data + ); + console.log("back to relay hub"); + console.log("success", success); + console.logBytes(retData); + console.log("****PreRelayed Call gas limit:", gasAndDataLimits.preRelayedCallGasLimit); + console.log("****Paymaster preRelayedCall Call:", success); + if (!success) { + GsnEip712Library.truncateInPlace(retData); + revertWithStatus(RelayCallStatus.RejectedByPreRelayed, retData); + } + (vars.recipientContext, vars.rejectOnRecipientRevert) = abi.decode(retData, (bytes, bool)); + } + + // The actual relayed call is now executed. The sender's address is appended at the end of the transaction data + + { + bool forwarderSuccess; + (forwarderSuccess, vars.relayedCallSuccess, vars.relayedCallReturnValue) = GsnEip712Library.execute( + domainSeparatorName, + relayRequest, + signature + ); + console.log("forwarderSuccess", forwarderSuccess); + console.log("relayedCallSuccess", vars.relayedCallSuccess); + if (!forwarderSuccess) { + revertWithStatus(RelayCallStatus.RejectedByForwarder, vars.relayedCallReturnValue); + } + + if (vars.rejectOnRecipientRevert && !vars.relayedCallSuccess) { + // we trusted the recipient, but it reverted... + revertWithStatus(RelayCallStatus.RejectedByRecipientRevert, vars.relayedCallReturnValue); + } + } + // Finally, postRelayedCall is executed, with the relayedCall execution's status and a charge estimate + // We now determine how much the recipient will be charged, to pass this value to postRelayedCall for accurate + // accounting. + vars.data = abi.encodeWithSelector( + IPaymaster.postRelayedCall.selector, + vars.recipientContext, + vars.relayedCallSuccess, + vars.gasUsedToCallInner + (vars.initialGasLeft - aggregateGasleft()), /*gasUseWithoutPost*/ + relayRequest.relayData + ); + + { + (bool successPost, bytes memory ret) = + relayRequest.relayData.paymaster.call{ gas: gasAndDataLimits.postRelayedCallGasLimit }(vars.data); + + if (!successPost) { + revertWithStatus(RelayCallStatus.PostRelayedFailed, ret); + } + } + + if (balances[relayRequest.relayData.paymaster] < vars.balanceBefore) { + revertWithStatus(RelayCallStatus.PaymasterBalanceChanged, ""); + } + + return ( + vars.relayedCallSuccess ? RelayCallStatus.OK : RelayCallStatus.RelayedCallFailed, + vars.relayedCallReturnValue + ); + } + + /** + * @dev Reverts the transaction with return data set to the ABI encoding of the status argument (and revert reason data) + */ + function revertWithStatus(RelayCallStatus status, bytes memory ret) private pure { + bytes memory data = abi.encode(status, ret); + GsnEip712Library.truncateInPlace(data); + + assembly { + let dataSize := mload(data) + let dataPtr := add(data, 32) + + revert(dataPtr, dataSize) + } + } + + /// @inheritdoc IRelayHub + function calculateDevCharge(uint256 charge) public view virtual override returns (uint256) { + if (config.devFee == 0) { + // save some gas in case of zero dev charge + return 0; + } + unchecked { return (charge * config.devFee) / 100; } + } + + /// @inheritdoc IRelayHub + function calculateCharge(uint256 gasUsed, GsnTypes.RelayData calldata relayData) + public + view + virtual + override + returns (uint256) + { + uint256 basefee; + if (relayData.maxFeePerGas == relayData.maxPriorityFeePerGas) { + basefee = 0; + } else { + basefee = block.basefee; + } + uint256 chargeableGasPrice = + Math.min(relayData.maxFeePerGas, Math.min(tx.gasprice, basefee + relayData.maxPriorityFeePerGas)); + return config.baseRelayFee + (gasUsed * chargeableGasPrice * (config.pctRelayFee + 100)) / 100; + } + + /// @inheritdoc IRelayHub + function verifyRelayManagerStaked(address relayManager) public view override { + (IStakeManager.StakeInfo memory info, bool isHubAuthorized) = stakeManager.getStakeInfo(relayManager); + uint256 minimumStake = minimumStakePerToken[info.token]; + require(info.token != IERC20(address(0)), "relay manager not staked"); + require(info.stake >= minimumStake, "stake amount is too small"); + require(minimumStake != 0, "staking this token is forbidden"); + require(info.unstakeDelay >= config.minimumUnstakeDelay, "unstake delay is too small"); + require(info.withdrawTime == 0, "stake has been withdrawn"); + require(isHubAuthorized, "this hub is not authorized by SM"); + } + + /// @inheritdoc IRelayHub + function deprecateHub(uint256 _deprecationTime) public override onlyOwner { + require(!isDeprecated(), "Already deprecated"); + deprecationTime = _deprecationTime; + emit HubDeprecated(deprecationTime); + } + + /// @inheritdoc IRelayHub + function isDeprecated() public view override returns (bool) { + return block.timestamp >= deprecationTime; + } + + /// @notice Prevents any address other than the `Penalizer` from calling this method. + modifier penalizerOnly() { + require(msg.sender == penalizer, "Not penalizer"); + _; + } + + /// @inheritdoc IRelayHub + function penalize(address relayWorker, address payable beneficiary) external override penalizerOnly { + address relayManager = workerToManager[relayWorker]; + // The worker must be controlled by a manager with a locked stake + require(relayManager != address(0), "Unknown relay worker"); + (IStakeManager.StakeInfo memory stakeInfo, ) = stakeManager.getStakeInfo(relayManager); + require(stakeInfo.stake > 0, "relay manager not staked"); + stakeManager.penalizeRelayManager(relayManager, beneficiary, stakeInfo.stake); + } + + /// @inheritdoc IRelayHub + function isRelayEscheatable(address relayManager) public view override returns (bool) { + return stakeManager.isRelayEscheatable(relayManager); + } + + /// @inheritdoc IRelayHub + function escheatAbandonedRelayBalance(address relayManager) external override onlyOwner { + require(stakeManager.isRelayEscheatable(relayManager), "relay server not escheatable yet"); + uint256 balance = balances[relayManager]; + balances[relayManager] = 0; + balances[config.devAddress] = balances[config.devAddress] + balance; + emit AbandonedRelayManagerBalanceEscheated(relayManager, balance); + } + + /// @inheritdoc IRelayHub + function aggregateGasleft() public view virtual override returns (uint256) { + return gasleft(); + } +} diff --git a/contracts/protocol/meta-tx/interfaces/IGsnRelayHub.sol b/contracts/protocol/meta-tx/GSN/interfaces/IGsnRelayHub.sol similarity index 100% rename from contracts/protocol/meta-tx/interfaces/IGsnRelayHub.sol rename to contracts/protocol/meta-tx/GSN/interfaces/IGsnRelayHub.sol diff --git a/contracts/protocol/meta-tx/interfaces/IPaymaster.sol b/contracts/protocol/meta-tx/GSN/interfaces/IPaymaster.sol similarity index 100% rename from contracts/protocol/meta-tx/interfaces/IPaymaster.sol rename to contracts/protocol/meta-tx/GSN/interfaces/IPaymaster.sol diff --git a/contracts/protocol/meta-tx/interfaces/IPenalizer.sol b/contracts/protocol/meta-tx/GSN/interfaces/IPenalizer.sol similarity index 100% rename from contracts/protocol/meta-tx/interfaces/IPenalizer.sol rename to contracts/protocol/meta-tx/GSN/interfaces/IPenalizer.sol diff --git a/contracts/protocol/meta-tx/interfaces/IRelayHub.sol b/contracts/protocol/meta-tx/GSN/interfaces/IRelayHub.sol similarity index 100% rename from contracts/protocol/meta-tx/interfaces/IRelayHub.sol rename to contracts/protocol/meta-tx/GSN/interfaces/IRelayHub.sol diff --git a/contracts/protocol/meta-tx/interfaces/IRelayRegistrar.sol b/contracts/protocol/meta-tx/GSN/interfaces/IRelayRegistrar.sol similarity index 100% rename from contracts/protocol/meta-tx/interfaces/IRelayRegistrar.sol rename to contracts/protocol/meta-tx/GSN/interfaces/IRelayRegistrar.sol diff --git a/contracts/protocol/meta-tx/interfaces/IStakeManager.sol b/contracts/protocol/meta-tx/GSN/interfaces/IStakeManager.sol similarity index 100% rename from contracts/protocol/meta-tx/interfaces/IStakeManager.sol rename to contracts/protocol/meta-tx/GSN/interfaces/IStakeManager.sol diff --git a/contracts/protocol/meta-tx/libraries/GsnEip712Library.sol b/contracts/protocol/meta-tx/GSN/libraries/GsnEip712Library.sol similarity index 99% rename from contracts/protocol/meta-tx/libraries/GsnEip712Library.sol rename to contracts/protocol/meta-tx/GSN/libraries/GsnEip712Library.sol index 8cde84790..6958d7e56 100644 --- a/contracts/protocol/meta-tx/libraries/GsnEip712Library.sol +++ b/contracts/protocol/meta-tx/GSN/libraries/GsnEip712Library.sol @@ -7,6 +7,7 @@ import "../interfaces/IERC2771Recipient.sol"; import "../interfaces/IForwarder.sol"; import "./GsnUtils.sol"; +import "hardhat/console.sol"; /** * @title The ERC-712 Library for GSN @@ -90,6 +91,7 @@ library GsnEip712Library { bytes memory ret ) { + console.log("EXECUTE!"); bytes memory suffixData = splitRequest(relayRequest); bytes32 _domainSeparator = domainSeparator(domainSeparatorName, relayRequest.relayData.forwarder); /* solhint-disable-next-line avoid-low-level-calls */ diff --git a/contracts/protocol/meta-tx/libraries/GsnTypes.sol b/contracts/protocol/meta-tx/GSN/libraries/GsnTypes.sol similarity index 100% rename from contracts/protocol/meta-tx/libraries/GsnTypes.sol rename to contracts/protocol/meta-tx/GSN/libraries/GsnTypes.sol diff --git a/contracts/protocol/meta-tx/libraries/GsnUtils.sol b/contracts/protocol/meta-tx/GSN/libraries/GsnUtils.sol similarity index 100% rename from contracts/protocol/meta-tx/libraries/GsnUtils.sol rename to contracts/protocol/meta-tx/GSN/libraries/GsnUtils.sol diff --git a/contracts/protocol/meta-tx/libraries/MinLibBytes.sol b/contracts/protocol/meta-tx/GSN/libraries/MinLibBytes.sol similarity index 100% rename from contracts/protocol/meta-tx/libraries/MinLibBytes.sol rename to contracts/protocol/meta-tx/GSN/libraries/MinLibBytes.sol diff --git a/contracts/protocol/meta-tx/GSN/libraries/RLPReader.sol b/contracts/protocol/meta-tx/GSN/libraries/RLPReader.sol new file mode 100644 index 000000000..afeac46c5 --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/libraries/RLPReader.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier:APACHE-2.0 +/* + * Taken from https://github.com/hamdiallam/Solidity-RLP + */ +/* solhint-disable */ +pragma solidity ^0.8.0; + +library RLPReader { + uint8 constant STRING_SHORT_START = 0x80; + uint8 constant STRING_LONG_START = 0xb8; + uint8 constant LIST_SHORT_START = 0xc0; + uint8 constant LIST_LONG_START = 0xf8; + uint8 constant WORD_SIZE = 32; + + struct RLPItem { + uint256 len; + uint256 memPtr; + } + + using RLPReader for bytes; + using RLPReader for uint256; + using RLPReader for RLPReader.RLPItem; + + // helper function to decode rlp encoded legacy ethereum transaction + /* + * @param rawTransaction RLP encoded legacy ethereum transaction rlp([nonce, gasPrice, gasLimit, to, value, data])) + * @return tuple (nonce,gasLimit,to,value,data) + */ + + function decodeLegacyTransaction(bytes calldata rawTransaction) + internal + pure + returns ( + uint256, + uint256, + address, + uint256, + bytes memory + ) + { + RLPReader.RLPItem[] memory values = rawTransaction.toRlpItem().toList(); // must convert to an rlpItem first! + return (values[0].toUint(), values[2].toUint(), values[3].toAddress(), values[4].toUint(), values[5].toBytes()); + } + + /* + * @param rawTransaction format: 0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, access_list])) + * @return tuple (nonce,gasLimit,to,value,data) + */ + function decodeTransactionType1(bytes calldata rawTransaction) + internal + pure + returns ( + uint256, + uint256, + address, + uint256, + bytes memory + ) + { + bytes memory payload = rawTransaction[1:rawTransaction.length]; + RLPReader.RLPItem[] memory values = payload.toRlpItem().toList(); // must convert to an rlpItem first! + return (values[1].toUint(), values[3].toUint(), values[4].toAddress(), values[5].toUint(), values[6].toBytes()); + } + + /* + * @param rawTransaction format: 0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list])) + * @return tuple (nonce,gasLimit,to,value,data) + */ + function decodeTransactionType2(bytes calldata rawTransaction) + internal + pure + returns ( + uint256, + uint256, + address, + uint256, + bytes memory + ) + { + bytes memory payload = rawTransaction[1:rawTransaction.length]; + RLPReader.RLPItem[] memory values = payload.toRlpItem().toList(); // must convert to an rlpItem first! + return (values[1].toUint(), values[4].toUint(), values[5].toAddress(), values[6].toUint(), values[7].toBytes()); + } + + /* + * @param item RLP encoded bytes + */ + function toRlpItem(bytes memory item) internal pure returns (RLPItem memory) { + if (item.length == 0) return RLPItem(0, 0); + uint256 memPtr; + assembly { + memPtr := add(item, 0x20) + } + return RLPItem(item.length, memPtr); + } + + /* + * @param item RLP encoded list in bytes + */ + function toList(RLPItem memory item) internal pure returns (RLPItem[] memory result) { + require(isList(item), "isList failed"); + uint256 items = numItems(item); + result = new RLPItem[](items); + uint256 memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 dataLen; + for (uint256 i = 0; i < items; i++) { + dataLen = _itemLength(memPtr); + result[i] = RLPItem(dataLen, memPtr); + memPtr = memPtr + dataLen; + } + } + + /* + * Helpers + */ + // @return indicator whether encoded payload is a list. negate this function call for isData. + function isList(RLPItem memory item) internal pure returns (bool) { + uint8 byte0; + uint256 memPtr = item.memPtr; + assembly { + byte0 := byte(0, mload(memPtr)) + } + if (byte0 < LIST_SHORT_START) return false; + return true; + } + + // @return number of payload items inside an encoded list. + function numItems(RLPItem memory item) internal pure returns (uint256) { + uint256 count = 0; + uint256 currPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 endPtr = item.memPtr + item.len; + while (currPtr < endPtr) { + currPtr = currPtr + _itemLength(currPtr); + // skip over an item + count++; + } + return count; + } + + // @return entire rlp item byte length + function _itemLength(uint256 memPtr) internal pure returns (uint256 len) { + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + if (byte0 < STRING_SHORT_START) return 1; + else if (byte0 < STRING_LONG_START) return byte0 - STRING_SHORT_START + 1; + else if (byte0 < LIST_SHORT_START) { + assembly { + let byteLen := sub(byte0, 0xb7) // number of bytes the actual length is + memPtr := add(memPtr, 1) // skip over the first byte + /* 32 byte word size */ + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len + len := add(dataLen, add(byteLen, 1)) + } + } else if (byte0 < LIST_LONG_START) { + return byte0 - LIST_SHORT_START + 1; + } else { + assembly { + let byteLen := sub(byte0, 0xf7) + memPtr := add(memPtr, 1) + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length + len := add(dataLen, add(byteLen, 1)) + } + } + } + + // @return number of bytes until the data + function _payloadOffset(uint256 memPtr) internal pure returns (uint256) { + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + if (byte0 < STRING_SHORT_START) return 0; + else if (byte0 < STRING_LONG_START || (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START)) return 1; + else if (byte0 < LIST_SHORT_START) + // being explicit + return byte0 - (STRING_LONG_START - 1) + 1; + else return byte0 - (LIST_LONG_START - 1) + 1; + } + + /** RLPItem conversions into data types **/ + // @returns raw rlp encoding in bytes + function toRlpBytes(RLPItem memory item) internal pure returns (bytes memory) { + bytes memory result = new bytes(item.len); + uint256 ptr; + assembly { + ptr := add(0x20, result) + } + copy(item.memPtr, ptr, item.len); + return result; + } + + function toBoolean(RLPItem memory item) internal pure returns (bool) { + require(item.len == 1, "Invalid RLPItem. Booleans are encoded in 1 byte"); + uint256 result; + uint256 memPtr = item.memPtr; + assembly { + result := byte(0, mload(memPtr)) + } + return result == 0 ? false : true; + } + + function toAddress(RLPItem memory item) internal pure returns (address) { + // 1 byte for the length prefix according to RLP spec + require(item.len <= 21, "Invalid RLPItem. Addresses are encoded in 20 bytes or less"); + return address(uint160(toUint(item))); + } + + function toUint(RLPItem memory item) internal pure returns (uint256) { + uint256 offset = _payloadOffset(item.memPtr); + uint256 len = item.len - offset; + uint256 memPtr = item.memPtr + offset; + uint256 result; + assembly { + result := div(mload(memPtr), exp(256, sub(32, len))) // shift to the correct location + } + return result; + } + + function toBytes(RLPItem memory item) internal pure returns (bytes memory) { + uint256 offset = _payloadOffset(item.memPtr); + uint256 len = item.len - offset; + // data length + bytes memory result = new bytes(len); + uint256 destPtr; + assembly { + destPtr := add(0x20, result) + } + copy(item.memPtr + offset, destPtr, len); + return result; + } + + /* + * @param src Pointer to source + * @param dest Pointer to destination + * @param len Amount of memory to copy from the source + */ + function copy( + uint256 src, + uint256 dest, + uint256 len + ) internal pure { + if (len == 0) return; + + // copy as many word sizes as possible + for (; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + if (len > 0) { + // left over bytes. Mask is used to remove unwanted bytes from the word + uint256 mask = 256**(WORD_SIZE - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + } + } +} diff --git a/contracts/protocol/meta-tx/GSN/libraries/RelayHubValidator.sol b/contracts/protocol/meta-tx/GSN/libraries/RelayHubValidator.sol new file mode 100644 index 000000000..333ef44f3 --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/libraries/RelayHubValidator.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "./GsnTypes.sol"; + +/** + * @title The RelayHub Validator Library + * @notice Validates the `msg.data` received by the `RelayHub` does not contain unnecessary bytes. + * Including these extra bytes would allow the Relay Server to inflate transaction costs and overcharge the client. + */ +library RelayHubValidator { + /// @notice Validate that encoded `relayCall` is properly packed without any extra bytes + function verifyTransactionPacking( + string calldata domainSeparatorName, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData + ) internal pure { + // abicoder v2: https://docs.soliditylang.org/en/latest/abi-spec.html + // each static param/member is 1 word + // struct (with dynamic members) has offset to struct which is 1 word + // dynamic member is 1 word offset to actual value, which is 1-word length and ceil(length/32) words for data + // relayCall has 5 method params, + // relayRequest: 2 members + // relayData 8 members + // ForwardRequest: 7 members + // total 21 32-byte words if all dynamic params are zero-length. + uint256 expectedMsgDataLen = + 4 + + 22 * + 32 + + dynamicParamSize(bytes(domainSeparatorName)) + + dynamicParamSize(signature) + + dynamicParamSize(approvalData) + + dynamicParamSize(relayRequest.request.data) + + dynamicParamSize(relayRequest.relayData.paymasterData); + // zero-length signature is allowed in a batch relay transaction + require(signature.length <= 65, "invalid signature length"); + require(expectedMsgDataLen == msg.data.length, "extra msg.data bytes"); + } + + // helper method for verifyTransactionPacking: + // size (in bytes) of the given "bytes" parameter. size include the length (32-byte word), + // and actual data size, rounded up to full 32-byte words + function dynamicParamSize(bytes calldata buf) internal pure returns (uint256) { + return 32 + ((buf.length + 31) & (type(uint256).max - 31)); + } +} From 618ecedea8e655fe346d36407858b14b7c4cdc8c Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Wed, 12 Oct 2022 06:43:35 -0400 Subject: [PATCH 18/30] fix(vault-gateway): change name --- contracts/protocol/meta-tx/VaultGateway.sol | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 contracts/protocol/meta-tx/VaultGateway.sol diff --git a/contracts/protocol/meta-tx/VaultGateway.sol b/contracts/protocol/meta-tx/VaultGateway.sol new file mode 100644 index 000000000..3dbdd524d --- /dev/null +++ b/contracts/protocol/meta-tx/VaultGateway.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { ERC2771Context } from "@openzeppelin/contracts-0.8.x/metatx/ERC2771Context.sol"; +import { SafeERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; +import { IVault } from "./interfaces/IVault.sol"; +import { IERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-0.8.x/token/ERC20/extensions/draft-IERC20Permit.sol"; +import { IERC20PermitLegacy } from "./interfaces/IERC20PermitLegacy.sol"; + +/** + * @title Vault contract inspired by AAVE V3's AToken.sol + * @author opty.fi + * @notice Implementation of the risk specific interest bearing vault + */ +contract VaultGateway is ERC2771Context { + using SafeERC20 for IERC20; + + // solhint-disable-next-line no-empty-blocks + constructor(address forwarder) ERC2771Context(forwarder) {} + + /** + * @notice Deposit underlying tokens to the a given OptyFi vault + * @param _vault the address of the target vault, + * @param _userDepositUT Amount in underlying token + * @param _expectedOutput Minimum amount of vault tokens minted after fees + * @param _permitParams permit parameters: amount, deadline, v, s, r + * @param _accountsProof merkle proof for caller + */ + function deposit( + address _vault, + uint256 _userDepositUT, + uint256 _expectedOutput, + bytes calldata _permitParams, + bytes32[] calldata _accountsProof + ) public returns (uint256) { + address _underlyingToken = IVault(_vault).underlyingToken(); + + _permit(_underlyingToken, _permitParams); + IERC20(_underlyingToken).safeTransferFrom(_msgSender(), address(this), _userDepositUT); + IERC20(_underlyingToken).approve(_vault, _userDepositUT); + + return IVault(_vault).userDepositVault(_msgSender(), _userDepositUT, _expectedOutput, "0x", _accountsProof); + } + + /* solhint-disable avoid-low-level-calls*/ + /** + * @dev execute the permit according to the permit param + * @param _permitParams data + */ + function _permit(address _token, bytes calldata _permitParams) internal { + if (_permitParams.length == 32 * 7) { + (bool success, ) = _token.call(abi.encodePacked(IERC20Permit.permit.selector, _permitParams)); + require(success, "!permit"); + } + + if (_permitParams.length == 32 * 8) { + (bool success, ) = _token.call(abi.encodePacked(IERC20PermitLegacy.permit.selector, _permitParams)); + require(success, "!legacy_permit"); + } + } +} From 9943fec6aac7f810748d2b0775426cbb848c0514 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Wed, 12 Oct 2022 06:57:35 -0400 Subject: [PATCH 19/30] fix(gsn): import --- contracts/mocks/contracts/TestHub.sol | 6 +++--- contracts/mocks/utils/RelayRegistrar.sol | 6 +++--- contracts/mocks/utils/StakeManager.sol | 2 +- contracts/mocks/utils/TokenGasCalculator.sol | 4 ++-- contracts/protocol/meta-tx/GSN/BasePaymaster.sol | 9 +-------- contracts/protocol/meta-tx/GSN/RelayHub.sol | 9 +-------- .../protocol/meta-tx/GSN/libraries/GsnEip712Library.sol | 9 ++------- contracts/protocol/meta-tx/GSN/libraries/GsnTypes.sol | 2 +- 8 files changed, 14 insertions(+), 33 deletions(-) diff --git a/contracts/mocks/contracts/TestHub.sol b/contracts/mocks/contracts/TestHub.sol index bc26a2343..450e1e3d2 100644 --- a/contracts/mocks/contracts/TestHub.sol +++ b/contracts/mocks/contracts/TestHub.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.0; pragma experimental ABIEncoderV2; -import "../../protocol/meta-tx/libraries/GsnTypes.sol"; -import "../../protocol/meta-tx/interfaces/IPaymaster.sol"; +import "../../protocol/meta-tx/GSN/libraries/GsnTypes.sol"; +import "../../protocol/meta-tx/GSN/interfaces/IPaymaster.sol"; -import "../../protocol/meta-tx/RelayHub.sol"; +import "../../protocol/meta-tx/GSN/RelayHub.sol"; import "../utils/AllEvents.sol"; diff --git a/contracts/mocks/utils/RelayRegistrar.sol b/contracts/mocks/utils/RelayRegistrar.sol index 8abd2fcaa..b55beab61 100644 --- a/contracts/mocks/utils/RelayRegistrar.sol +++ b/contracts/mocks/utils/RelayRegistrar.sol @@ -10,9 +10,9 @@ import "hardhat/console.sol"; import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; import "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; -import "../../protocol/meta-tx/libraries/MinLibBytes.sol"; -import "../../protocol/meta-tx/interfaces/IRelayHub.sol"; -import "../../protocol/meta-tx/interfaces/IRelayRegistrar.sol"; +import "../../protocol/meta-tx/GSN/libraries/MinLibBytes.sol"; +import "../../protocol/meta-tx/GSN/interfaces/IRelayHub.sol"; +import "../../protocol/meta-tx/GSN/interfaces/IRelayRegistrar.sol"; /** * @title The RelayRegistrar Implementation diff --git a/contracts/mocks/utils/StakeManager.sol b/contracts/mocks/utils/StakeManager.sol index a74a2a60a..eb8df95ba 100644 --- a/contracts/mocks/utils/StakeManager.sol +++ b/contracts/mocks/utils/StakeManager.sol @@ -7,7 +7,7 @@ import "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; import "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; -import "../../protocol/meta-tx/interfaces/IStakeManager.sol"; +import "../../protocol/meta-tx/GSN/interfaces/IStakeManager.sol"; /** * @title The StakeManager implementation diff --git a/contracts/mocks/utils/TokenGasCalculator.sol b/contracts/mocks/utils/TokenGasCalculator.sol index a9eced211..fe654792f 100644 --- a/contracts/mocks/utils/TokenGasCalculator.sol +++ b/contracts/mocks/utils/TokenGasCalculator.sol @@ -4,8 +4,8 @@ pragma experimental ABIEncoderV2; import "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; -import "../../protocol/meta-tx/RelayHub.sol"; -import "../../protocol/meta-tx/BasePaymaster.sol"; +import "../../protocol/meta-tx/GSN/RelayHub.sol"; +import "../../protocol/meta-tx/GSN/BasePaymaster.sol"; import "./AllEvents.sol"; /** diff --git a/contracts/protocol/meta-tx/GSN/BasePaymaster.sol b/contracts/protocol/meta-tx/GSN/BasePaymaster.sol index ceb82e714..b5fbe9449 100644 --- a/contracts/protocol/meta-tx/GSN/BasePaymaster.sol +++ b/contracts/protocol/meta-tx/GSN/BasePaymaster.sol @@ -10,8 +10,7 @@ import "./libraries/GsnTypes.sol"; import "./interfaces/IPaymaster.sol"; import "./interfaces/IRelayHub.sol"; import "./libraries/GsnEip712Library.sol"; -import "./interfaces/IForwarder.sol"; -import "hardhat/console.sol"; +import "../interfaces/IForwarder.sol"; /** * @notice An abstract base class to be inherited by a concrete Paymaster. @@ -130,17 +129,11 @@ abstract contract BasePaymaster is IPaymaster, Ownable, ERC165 { bytes calldata approvalData, uint256 maxPossibleGas ) external override returns (bytes memory, bool) { - console.log("_verifyRelayHubOnly"); _verifyRelayHubOnly(); - console.log("_verifyForwarder"); _verifyForwarder(relayRequest); - console.log("_verifyValue"); _verifyValue(relayRequest); - console.log("_verifyPaymasterData"); _verifyPaymasterData(relayRequest); - console.log("_verifyApprovalData"); _verifyApprovalData(approvalData); - console.log("_preRelayedCall"); return _preRelayedCall(relayRequest, signature, approvalData, maxPossibleGas); } diff --git a/contracts/protocol/meta-tx/GSN/RelayHub.sol b/contracts/protocol/meta-tx/GSN/RelayHub.sol index 026a1d680..ab3698ec0 100644 --- a/contracts/protocol/meta-tx/GSN/RelayHub.sol +++ b/contracts/protocol/meta-tx/GSN/RelayHub.sol @@ -25,7 +25,7 @@ import "./libraries/RelayHubValidator.sol"; import "./libraries/GsnTypes.sol"; import "./interfaces/IRelayHub.sol"; import "./interfaces/IPaymaster.sol"; -import "./interfaces/IForwarder.sol"; +import "../interfaces/IForwarder.sol"; import "./interfaces/IStakeManager.sol"; import "./interfaces/IRelayRegistrar.sol"; import "./interfaces/IStakeManager.sol"; @@ -511,11 +511,6 @@ contract RelayHub is IRelayHub, Ownable, ERC165 { (success, retData) = relayRequest.relayData.paymaster.call{ gas: gasAndDataLimits.preRelayedCallGasLimit }( vars.data ); - console.log("back to relay hub"); - console.log("success", success); - console.logBytes(retData); - console.log("****PreRelayed Call gas limit:", gasAndDataLimits.preRelayedCallGasLimit); - console.log("****Paymaster preRelayedCall Call:", success); if (!success) { GsnEip712Library.truncateInPlace(retData); revertWithStatus(RelayCallStatus.RejectedByPreRelayed, retData); @@ -532,8 +527,6 @@ contract RelayHub is IRelayHub, Ownable, ERC165 { relayRequest, signature ); - console.log("forwarderSuccess", forwarderSuccess); - console.log("relayedCallSuccess", vars.relayedCallSuccess); if (!forwarderSuccess) { revertWithStatus(RelayCallStatus.RejectedByForwarder, vars.relayedCallReturnValue); } diff --git a/contracts/protocol/meta-tx/GSN/libraries/GsnEip712Library.sol b/contracts/protocol/meta-tx/GSN/libraries/GsnEip712Library.sol index 6958d7e56..81addb39b 100644 --- a/contracts/protocol/meta-tx/GSN/libraries/GsnEip712Library.sol +++ b/contracts/protocol/meta-tx/GSN/libraries/GsnEip712Library.sol @@ -3,11 +3,10 @@ pragma solidity ^0.8.0; pragma abicoder v2; import "./GsnTypes.sol"; -import "../interfaces/IERC2771Recipient.sol"; -import "../interfaces/IForwarder.sol"; +import "../../interfaces/IERC2771Recipient.sol"; +import "../../interfaces/IForwarder.sol"; import "./GsnUtils.sol"; -import "hardhat/console.sol"; /** * @title The ERC-712 Library for GSN @@ -54,9 +53,6 @@ library GsnEip712Library { relayRequest.request.to.staticcall( abi.encodeWithSelector(IERC2771Recipient.isTrustedForwarder.selector, relayRequest.relayData.forwarder) ); - require(success, "isTrustedForwarder: reverted"); - require(ret.length == 32, "isTrustedForwarder: bad response"); - require(abi.decode(ret, (bool)), "invalid forwarder for recipient"); } function verifySignature( @@ -91,7 +87,6 @@ library GsnEip712Library { bytes memory ret ) { - console.log("EXECUTE!"); bytes memory suffixData = splitRequest(relayRequest); bytes32 _domainSeparator = domainSeparator(domainSeparatorName, relayRequest.relayData.forwarder); /* solhint-disable-next-line avoid-low-level-calls */ diff --git a/contracts/protocol/meta-tx/GSN/libraries/GsnTypes.sol b/contracts/protocol/meta-tx/GSN/libraries/GsnTypes.sol index b4d498813..6f2b8bee5 100644 --- a/contracts/protocol/meta-tx/GSN/libraries/GsnTypes.sol +++ b/contracts/protocol/meta-tx/GSN/libraries/GsnTypes.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import "../interfaces/IForwarder.sol"; +import "../../interfaces/IForwarder.sol"; library GsnTypes { struct RelayData { From d1a247c3958762a4770bc65ff2863316920e2a15 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Wed, 12 Oct 2022 06:59:09 -0400 Subject: [PATCH 20/30] feat(forwarder): add contract --- contracts/protocol/meta-tx/Forwarder.sol | 182 +++++++++++++++++++++++ contracts/protocol/meta-tx/MetaVault.sol | 63 -------- 2 files changed, 182 insertions(+), 63 deletions(-) create mode 100644 contracts/protocol/meta-tx/Forwarder.sol delete mode 100644 contracts/protocol/meta-tx/MetaVault.sol diff --git a/contracts/protocol/meta-tx/Forwarder.sol b/contracts/protocol/meta-tx/Forwarder.sol new file mode 100644 index 000000000..5268f39a5 --- /dev/null +++ b/contracts/protocol/meta-tx/Forwarder.sol @@ -0,0 +1,182 @@ +// solhint-disable not-rely-on-time +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; + +import "./interfaces/IForwarder.sol"; + +/** + * @title The Forwarder Implementation + * @notice This implementation of the `IForwarder` interface uses ERC-712 signatures and stored nonces for verification. + */ +contract Forwarder is IForwarder, ERC165 { + using ECDSA for bytes32; + + address private constant DRY_RUN_ADDRESS = 0x0000000000000000000000000000000000000000; + + string public constant GENERIC_PARAMS = + "address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime"; + + string public constant EIP712_DOMAIN_TYPE = + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; + + mapping(bytes32 => bool) public typeHashes; + mapping(bytes32 => bool) public domains; + + // Nonces of senders, used to prevent replay attacks + mapping(address => uint256) private nonces; + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} + + /// @inheritdoc IForwarder + function getNonce(address from) public view override returns (uint256) { + return nonces[from]; + } + + constructor() { + string memory requestType = string(abi.encodePacked("ForwardRequest(", GENERIC_PARAMS, ")")); + registerRequestTypeInternal(requestType); + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(IForwarder).interfaceId || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IForwarder + function verify( + ForwardRequest calldata req, + bytes32 domainSeparator, + bytes32 requestTypeHash, + bytes calldata suffixData, + bytes calldata sig + ) external view override { + _verifyNonce(req); + _verifySig(req, domainSeparator, requestTypeHash, suffixData, sig); + } + + /// @inheritdoc IForwarder + function execute( + ForwardRequest calldata req, + bytes32 domainSeparator, + bytes32 requestTypeHash, + bytes calldata suffixData, + bytes calldata sig + ) external payable override returns (bool success, bytes memory ret) { + _verifySig(req, domainSeparator, requestTypeHash, suffixData, sig); + _verifyAndUpdateNonce(req); + + require(req.validUntilTime == 0 || req.validUntilTime > block.timestamp, "FWD: request expired"); + + uint256 gasForTransfer = 0; + if (req.value != 0) { + gasForTransfer = 40000; //buffer in case we need to move eth after the transaction. + } + bytes memory callData = abi.encodePacked(req.data, req.from); + require((gasleft() * 63) / 64 >= req.gas + gasForTransfer, "FWD: insufficient gas"); + // solhint-disable-next-line avoid-low-level-calls + (success, ret) = req.to.call{ gas: req.gas, value: req.value }(callData); + + if (req.value != 0 && address(this).balance > 0) { + // can't fail: req.from signed (off-chain) the request, so it must be an EOA... + payable(req.from).transfer(address(this).balance); + } + + return (success, ret); + } + + function _verifyNonce(ForwardRequest calldata req) internal view { + require(nonces[req.from] == req.nonce, "FWD: nonce mismatch"); + } + + function _verifyAndUpdateNonce(ForwardRequest calldata req) internal { + require(nonces[req.from]++ == req.nonce, "FWD: nonce mismatch"); + } + + /// @inheritdoc IForwarder + function registerRequestType(string calldata typeName, string calldata typeSuffix) external override { + for (uint256 i = 0; i < bytes(typeName).length; i++) { + bytes1 c = bytes(typeName)[i]; + require(c != "(" && c != ")", "FWD: invalid typename"); + } + + string memory requestType = string(abi.encodePacked(typeName, "(", GENERIC_PARAMS, ",", typeSuffix)); + registerRequestTypeInternal(requestType); + } + + /// @inheritdoc IForwarder + function registerDomainSeparator(string calldata name, string calldata version) external override { + uint256 chainId; + /* solhint-disable-next-line no-inline-assembly */ + assembly { + chainId := chainid() + } + + bytes memory domainValue = + abi.encode( + keccak256(bytes(EIP712_DOMAIN_TYPE)), + keccak256(bytes(name)), + keccak256(bytes(version)), + chainId, + address(this) + ); + + bytes32 domainHash = keccak256(domainValue); + + domains[domainHash] = true; + emit DomainRegistered(domainHash, domainValue); + } + + function registerRequestTypeInternal(string memory requestType) internal { + bytes32 requestTypehash = keccak256(bytes(requestType)); + typeHashes[requestTypehash] = true; + emit RequestTypeRegistered(requestTypehash, requestType); + } + + function _verifySig( + ForwardRequest calldata req, + bytes32 domainSeparator, + bytes32 requestTypeHash, + bytes calldata suffixData, + bytes calldata sig + ) internal view virtual { + require(domains[domainSeparator], "FWD: unregistered domain sep."); + require(typeHashes[requestTypeHash], "FWD: unregistered typehash"); + + bytes32 digest = + keccak256( + abi.encodePacked("\x19\x01", domainSeparator, keccak256(_getEncoded(req, requestTypeHash, suffixData))) + ); + // solhint-disable-next-line avoid-tx-origin + require(tx.origin == DRY_RUN_ADDRESS || digest.recover(sig) == req.from, "FWD: signature mismatch"); + } + + /** + * @notice Creates a byte array that is a valid ABI encoding of a request of a `RequestType` type. See `execute()`. + */ + function _getEncoded( + ForwardRequest calldata req, + bytes32 requestTypeHash, + bytes calldata suffixData + ) public pure returns (bytes memory) { + // we use encodePacked since we append suffixData as-is, not as dynamic param. + // still, we must make sure all first params are encoded as abi.encode() + // would encode them - as 256-bit-wide params. + return + abi.encodePacked( + requestTypeHash, + uint256(uint160(req.from)), + uint256(uint160(req.to)), + req.value, + req.gas, + req.nonce, + keccak256(req.data), + req.validUntilTime, + suffixData + ); + } +} diff --git a/contracts/protocol/meta-tx/MetaVault.sol b/contracts/protocol/meta-tx/MetaVault.sol deleted file mode 100644 index 81838fb45..000000000 --- a/contracts/protocol/meta-tx/MetaVault.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import { ERC2771Context } from "@openzeppelin/contracts-0.8.x/metatx/ERC2771Context.sol"; -import { MinimalForwarder } from "@openzeppelin/contracts-0.8.x/metatx/MinimalForwarder.sol"; -import { SafeERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; -import { IVault } from "./interfaces/IVault.sol"; -import { IERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; -import { IERC20Permit } from "@openzeppelin/contracts-0.8.x/token/ERC20/extensions/draft-IERC20Permit.sol"; -import { IERC20PermitLegacy } from "./interfaces/IERC20PermitLegacy.sol"; - -/** - * @title Vault contract inspired by AAVE V3's AToken.sol - * @author opty.fi - * @notice Implementation of the risk specific interest bearing vault - */ -contract MetaVault is ERC2771Context { - using SafeERC20 for IERC20; - - // solhint-disable-next-line no-empty-blocks - constructor(address forwarder) ERC2771Context(forwarder) {} - - /** - * @notice Deposit underlying tokens to the a given OptyFi vault - * @param _vault the address of the target vault, - * @param _userDepositUT Amount in underlying token - * @param _expectedOutput Minimum amount of vault tokens minted after fees - * @param _permitParams permit parameters: amount, deadline, v, s, r - * @param _accountsProof merkle proof for caller - */ - function deposit( - address _vault, - uint256 _userDepositUT, - uint256 _expectedOutput, - bytes calldata _permitParams, - bytes32[] calldata _accountsProof - ) public returns (uint256) { - address _underlyingToken = IVault(_vault).underlyingToken(); - - _permit(_underlyingToken, _permitParams); - IERC20(_underlyingToken).safeTransferFrom(_msgSender(), address(this), _userDepositUT); - IERC20(_underlyingToken).approve(_vault, _userDepositUT); - - return IVault(_vault).userDepositVault(_msgSender(), _userDepositUT, _expectedOutput, "0x", _accountsProof); - } - - /* solhint-disable avoid-low-level-calls*/ - /** - * @dev execute the permit according to the permit param - * @param _permitParams data - */ - function _permit(address _token, bytes calldata _permitParams) internal { - if (_permitParams.length == 32 * 7) { - (bool success, ) = _token.call(abi.encodePacked(IERC20Permit.permit.selector, _permitParams)); - require(success, "!permit"); - } - - if (_permitParams.length == 32 * 8) { - (bool success, ) = _token.call(abi.encodePacked(IERC20PermitLegacy.permit.selector, _permitParams)); - require(success, "!legacy_permit"); - } - } -} From 5c41efed7ae37d8bc0cf13b0be2c0617ab6562cd Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Wed, 12 Oct 2022 07:01:08 -0400 Subject: [PATCH 21/30] refactor(paymaster): remove opty swapper --- contracts/protocol/meta-tx/TokenPaymaster.sol | 172 ++++++++++-------- 1 file changed, 96 insertions(+), 76 deletions(-) diff --git a/contracts/protocol/meta-tx/TokenPaymaster.sol b/contracts/protocol/meta-tx/TokenPaymaster.sol index 70ddef181..0cd9d3114 100644 --- a/contracts/protocol/meta-tx/TokenPaymaster.sol +++ b/contracts/protocol/meta-tx/TokenPaymaster.sol @@ -5,19 +5,21 @@ pragma experimental ABIEncoderV2; //contracts import { Ownable } from "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; import { ERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/ERC20.sol"; -import { BasePaymaster } from "./BasePaymaster.sol"; +import { BasePaymaster } from "./GSN/BasePaymaster.sol"; //libraries import { SafeMath } from "@openzeppelin/contracts-0.8.x/utils/math/SafeMath.sol"; import { SafeERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; -import { GsnTypes } from "./libraries/GsnTypes.sol"; -import { DataTypes as SwapTypes } from "../optyfi-swapper/contracts/swap/DataTypes.sol"; +import { GsnTypes } from "./GSN/libraries/GsnTypes.sol"; //interfaces -import { ISwapper } from "../optyfi-swapper/contracts/swap/ISwapper.sol"; import { IOptyFiOracle } from "../optyfi-oracle/contracts/interfaces/IOptyFiOracle.sol"; import { IERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-0.8.x/token/ERC20/extensions/draft-IERC20Permit.sol"; +import { IERC20PermitLegacy } from "../../interfaces/opty/IERC20PermitLegacy-0.8.x.sol"; import { IForwarder } from "./interfaces/IForwarder.sol"; +import { IUniswapV2Router01 } from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router01.sol"; +import { ISwapRouter } from "../../interfaces/uniswap/ISwapRouter.sol"; /** * @title A Token-based paymaster @@ -27,7 +29,7 @@ import { IForwarder } from "./interfaces/IForwarder.sol"; * - postRelayedCall - refund the caller for the unused gas */ contract TokenPaymaster is BasePaymaster { - using SafeERC20 for IERC20; + // using SafeERC20 for IERC20; using SafeMath for uint256; /** @dev ETH address */ @@ -36,13 +38,22 @@ contract TokenPaymaster is BasePaymaster { /** WETH ERC20 token address */ address public constant WETH = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - ISwapper optyFiSwapper; + struct PaymasterData { + address token; + address dex; + bool isUniV3; + bytes permit; + bytes approve; + bytes pathUniv3; + address[] pathUniV2; + uint256 deadline; + } + IOptyFiOracle optyFiOracle; uint256 public gasUsedByPost; - constructor(address _optyFiswapper, address _optyFiOracle) { + constructor(address _optyFiOracle) { optyFiOracle = IOptyFiOracle(_optyFiOracle); - optyFiSwapper = ISwapper(_optyFiswapper); } function versionPaymaster() external view virtual override returns (string memory) { @@ -67,20 +78,12 @@ contract TokenPaymaster is BasePaymaster { optyFiOracle = IOptyFiOracle(_optyFiOracle); } - /** - * @notice Sets the OptyFi Swapper contract - * @param _optyFiSwapper OptyFi Swapper contract address - */ - function setOptyFiSwapper(address _optyFiSwapper) external onlyOwner { - optyFiSwapper = ISwapper(_optyFiSwapper); - } - /** * @notice return the payer of a given RelayRequest * @param _relayRequest GSN RelayRequest */ function getPayer(GsnTypes.RelayRequest calldata _relayRequest) public view virtual returns (address) { - return _relayRequest.request.to; + return _relayRequest.request.from; } /** @@ -88,32 +91,16 @@ contract TokenPaymaster is BasePaymaster { * @param _paymasterData paymaster data containing the swap data */ function _getToken(bytes memory _paymasterData) internal view returns (IERC20 token) { - (address _token, , , , , , ) = - abi.decode(_paymasterData, (address, address[], bytes, uint256[], uint256[], bytes, uint256)); - token = IERC20(token); + PaymasterData memory paymasterData = abi.decode(_paymasterData, (PaymasterData)); + token = IERC20(paymasterData.token); } /** * @dev decode paymasterData * @param _paymasterData swap data (token, callees, exchangeData, startIndexes, values, permit, deadline) */ - function _getSwapData(bytes memory _paymasterData) - internal - view - returns ( - address token, - address[] memory callees, - bytes memory exchangeData, - uint256[] memory startIndexes, - uint256[] memory values, - bytes memory permit, - uint256 deadline - ) - { - (token, callees, exchangeData, startIndexes, values, permit, deadline) = abi.decode( - _paymasterData, - (address, address[], bytes, uint256[], uint256[], bytes, uint256) - ); + function _getPaymasterData(bytes memory _paymasterData) internal view returns (PaymasterData memory paymasterData) { + paymasterData = abi.decode(_paymasterData, (PaymasterData)); } /** @@ -147,10 +134,10 @@ contract TokenPaymaster is BasePaymaster { uint256 _maxPossibleGas ) internal virtual override returns (bytes memory context, bool revertOnRecipientRevert) { (_signature, _approvalData); - IERC20 token = _getToken(_relayRequest.relayData.paymasterData); (address payer, uint256 tokenPrecharge) = _calculatePreCharge(address(token), _relayRequest, _maxPossibleGas); - token.safeTransferFrom(payer, address(this), tokenPrecharge); + bool success = token.transferFrom(payer, address(this), tokenPrecharge); + uint256 x = 10; return (abi.encode(payer, tokenPrecharge, token), false); } @@ -189,9 +176,10 @@ contract TokenPaymaster is BasePaymaster { ) internal { uint256 ethActualCharge = relayHub.calculateCharge(_gasUseWithoutPost.add(gasUsedByPost), _relayData); uint256 tokenActualCharge = _getETHInToken(address(_token), _valueRequested.add(ethActualCharge)); + uint256 tokenRefund = _tokenPrecharge.sub(tokenActualCharge); _refundPayer(_payer, _token, tokenRefund); - _depositProceedsToHub(ethActualCharge, _relayData.paymasterData); + _depositProceedsToHub(_payer, ethActualCharge, _relayData.paymasterData); emit TokensCharged(_gasUseWithoutPost, gasUsedByPost, ethActualCharge, tokenActualCharge); } @@ -214,49 +202,48 @@ contract TokenPaymaster is BasePaymaster { * @param _ethActualCharge gas cost paid by the relayer * @param _paymasterData swap data (token, callees, exchangeData, startIndexes, values, permit, deadline) */ - function _depositProceedsToHub(uint256 _ethActualCharge, bytes calldata _paymasterData) private { - uint256 receivedAmount = _swapToETH(_ethActualCharge, _paymasterData); + function _depositProceedsToHub( + address _payer, + uint256 _ethActualCharge, + bytes calldata _paymasterData + ) private { + uint256 receivedAmount = _swapToETH(_payer, _ethActualCharge, _paymasterData); relayHub.depositFor{ value: receivedAmount }(address(this)); } /** * @dev swap token for ETH * @param _ethActualCharge gas cost paid by the relayer - * @param _paymasterData swap data (token, callees, exchangeData, startIndexes, values, permit, deadline) + * @param _paymasterData swap data (token, router, permit) * @return receivedAmount amount received from the swap */ - function _swapToETH(uint256 _ethActualCharge, bytes calldata _paymasterData) - private - returns (uint256 receivedAmount) - { - ( - address token, - address[] memory callees, - bytes memory exchangeData, - uint256[] memory startIndexes, - uint256[] memory values, - bytes memory permit, - uint256 deadline - ) = _getSwapData(_paymasterData); - uint256 tokenAmount = _getETHInToken(token, _ethActualCharge); - SwapTypes.SwapData memory swapData = - SwapTypes.SwapData( - token, - ETH, - tokenAmount, - _ethActualCharge, - callees, - exchangeData, - startIndexes, - values, - payable(address(this)), - permit, - deadline + function _swapToETH( + address _payer, + uint256 _ethActualCharge, + bytes calldata _paymasterData + ) private returns (uint256 receivedAmount) { + PaymasterData memory pd = _getPaymasterData(_paymasterData); + _approve(pd.token, pd.approve); + _permit(pd.token, pd.permit); + uint256[] memory amounts; + if (pd.isUniV3) { + ISwapRouter(pd.dex).exactInput( + ISwapRouter.ExactInputParams({ + path: pd.pathUniv3, + recipient: address(this), + deadline: pd.deadline, + amountIn: _getETHInToken(pd.token, _ethActualCharge).mul(101).div(100), + amountOutMinimum: 0 //_ethActualCharge + }) + ); + } else { + amounts = IUniswapV2Router01(pd.dex).swapExactTokensForETH( + _getETHInToken(pd.token, _ethActualCharge).mul(101).div(100), + 0, //_ethActualCharge, + pd.pathUniV2, + address(this), + pd.deadline ); - uint256 unused; - (receivedAmount, unused) = optyFiSwapper.swap(swapData); - if (unused > 0) { - IERC20(token).safeTransfer(msg.sender, unused); } } @@ -274,8 +261,9 @@ contract TokenPaymaster is BasePaymaster { ) internal view returns (uint256 _swapOutAmount) { uint256 price = optyFiOracle.getTokenPrice(_token0, _token1); require(price > uint256(0), "!price"); - uint256 decimals0 = uint256(ERC20(_token0).decimals()); - uint256 decimals1 = uint256(ERC20(_token1).decimals()); + uint256 decimals0 = ERC20(_token0).decimals(); + uint256 decimals1 = ERC20(_token1).decimals(); + _swapOutAmount = ((_swapInAmount * price * 10**decimals1) / 10**(18 + decimals0)); } @@ -289,6 +277,38 @@ contract TokenPaymaster is BasePaymaster { _token == WETH ? amount = _amount : amount = _calculateSwapOutAmount(_amount, WETH, _token); } + /* solhint-disable avoid-low-level-calls*/ + /** + * @dev execute the permit according to the permit data + * @param _permitData data + */ + function _permit(address _token, bytes memory _permitData) internal { + if (_permitData.length == 32 * 7) { + (bool success, ) = _token.call(abi.encodePacked(IERC20Permit.permit.selector, _permitData)); + require(success, "PERMIT_FAILED"); + } + + if (_permitData.length == 32 * 8) { + (bool success, ) = _token.call(abi.encodePacked(IERC20PermitLegacy.permit.selector, _permitData)); + require(success, "PERMIT_LEGACY_FAILED"); + } + } + + /** + * @dev execute the approve according to the approve data + * @param _approveData data + */ + function _approve(address _token, bytes memory _approveData) internal { + if (_approveData.length < 32 * 7) { + (bool success, ) = _token.call(abi.encodePacked(IERC20.approve.selector, _approveData)); + require(success, "APPROVE_FAILED"); + } + } + + function _verifyPaymasterData(GsnTypes.RelayRequest calldata relayRequest) internal view override { + require(relayRequest.relayData.paymasterData.length == 544, "invalid paymasterData"); + } + receive() external payable override { emit Received(msg.value); } From d28ae53ad08f96710dcf9804fd2eb4afe3048d8d Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Wed, 12 Oct 2022 07:03:42 -0400 Subject: [PATCH 22/30] fix(utils): gsn utils --- test/test-opty/utils.ts | 166 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 155 insertions(+), 11 deletions(-) diff --git a/test/test-opty/utils.ts b/test/test-opty/utils.ts index 10e86339d..dd512751c 100644 --- a/test/test-opty/utils.ts +++ b/test/test-opty/utils.ts @@ -1,6 +1,6 @@ // !! Important !! // Please do not keep this file under helpers/utils as it imports hre from hardhat -import { BigNumber, BigNumberish, Contract, Signature } from "ethers"; +import { BigNumber, BigNumberish, Contract, Signature, Signer } from "ethers"; import { getAddress, parseEther, splitSignature } from "ethers/lib/utils"; import hre, { ethers } from "hardhat"; import ethTokens from "@optyfi/defi-legos/ethereum/tokens/wrapped_tokens"; @@ -8,8 +8,33 @@ import polygonTokens from "@optyfi/defi-legos/polygon/tokens"; import avaxTokens from "@optyfi/defi-legos/avalanche/tokens"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; import { StrategyStepType } from "../../helpers/type"; -import { ERC20, IAdapterFull, IWETH, Registry, ERC20Permit } from "../../typechain"; +import { + ERC20, + IAdapterFull, + IWETH, + Registry, + ERC20Permit, + RelayHub__factory, + RelayRegistrar__factory, +} from "../../typechain"; import { fundWalletToken, getBlockTimestamp } from "../../helpers/contracts-actions"; +import { StakeManager, TokenGasCalculator, RelayHub, TestToken, RelayRegistrar } from "../../typechain"; +import { deployContract } from "../../helpers/helpers"; +import { PrefixedHexString } from "ethereumjs-util"; +import { GasUsedEvent } from "../../typechain/TokenGasCalculator"; +import { + Environment, + IntString, + RelayHubConfiguration, + constants, + ForwardRequest, + RelayData, + RelayRequest, + defaultEnvironment, + splitRelayUrlForRegistrar, + TypedRequestData, +} from "@opengsn/common"; +import { defaultGsnConfig } from "@opengsn/provider"; const setStorageAt = (address: string, slot: string, val: string): Promise => hre.network.provider.send("hardhat_setStorageAt", [address, slot, val]); @@ -307,43 +332,55 @@ const EIP712Domain = [ { name: "verifyingContract", type: "address" }, ]; -const ForwardRequest = [ +const RelayRequest = [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "gas", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "data", type: "bytes" }, + { name: "validUntilTime", type: "uint256" }, +]; + +const RelayData = [ + { name: "maxFeePerGas", type: "uint256" }, + { name: "maxPriorityFeePerGas", type: "uint256" }, + { name: "transactionCalldataGasUsed", type: "uint256" }, + { name: "relayWorker", type: "address" }, + { name: "paymaster", type: "address" }, + { name: "forwarder", type: "address" }, + { name: "paymasterData", type: "bytes" }, + { name: "clientId", type: "uint256" }, ]; function getMetaTxTypeData(chainId: number, verifyingContract: string) { return { types: { EIP712Domain, - ForwardRequest, + RelayRequest, }, domain: { - name: "MinimalForwarder", - version: "0.0.1", + name: "GSN Relayed Transaction", + version: 3, chainId, verifyingContract, }, - primaryType: "ForwardRequest", + primaryType: "RelayRequest", }; } -async function signTypedData(signer: any, from: string, data: any) { +export async function signTypedData(signer: any, from: string, data: any) { const isHardhat = data.domain.chainId == 31337; const [method, argData] = isHardhat ? ["eth_signTypedData", data] : ["eth_signTypedData_v4", JSON.stringify(data)]; return await signer.send(method, [from, argData]); } -async function buildRequest(forwarder: Contract, input: any) { +export async function buildRequest(forwarder: Contract, input: any) { const nonce = (await forwarder.getNonce(input.from)).toString(); - return { value: 0, gas: 1e6, nonce, ...input }; + return { value: 0, gas: 1e6, nonce, validUntilTime: ethers.constants.MaxUint256, ...input }; } -async function buildTypedData(forwarder: Contract, request: any) { +export async function buildTypedData(forwarder: Contract, request: any) { const chainId = (await forwarder.provider.getNetwork()).chainId; const typeData = getMetaTxTypeData(chainId, forwarder.address); return { ...typeData, message: request }; @@ -412,3 +449,110 @@ export async function getPermitLegacySignature( ), ); } + +export async function revertReason(func: Promise): Promise { + try { + await func; + return "ok"; // no revert + } catch (e: any) { + return e.message.replace(/.*reverted with reason string /, ""); + } +} + +export async function registerAsRelayServer( + token: ERC20Permit, + stakeManager: StakeManager, + relay: SignerWithAddress, + relayOwner: SignerWithAddress, + stake: string, + hub: RelayHub, +): Promise { + console.log("1.1"); + await stakeManager.connect(relay).setRelayManagerOwner(relayOwner.address); + console.log("1.2"); + await stakeManager + .connect(relayOwner) + .stakeForRelayManager(token.address, relay.address, 7 * 24 * 3600, stake.toString()); + console.log("1.3"); + await stakeManager.connect(relayOwner).authorizeHubByOwner(relay.address, hub.address); + console.log("1.4"); + await hub.setMinimumStakes([token.address], [stake]); + console.log("1.5"); + await hub.connect(relay).addRelayWorkers([relay.address]); + console.log("1.6"); + const relayRegistrar = ( + await hre.ethers.getContractAt("RelayRegistrar", await hub.getRelayRegistrar()) + ); + console.log("1.7"); + await relayRegistrar.connect(relay).registerRelayServer(hub.address, splitRelayUrlForRegistrar("url")); +} + +export async function deployTestHub(calculator: boolean = false, signer: Signer): Promise { + const contract = calculator ? "TokenGasCalculator" : "TestHub"; + return deployContract(hre, contract, false, signer, [ + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + defaultEnvironment.relayHubConfiguration, + ]); +} + +export function mergeRelayRequest( + req: RelayRequest, + overrideData: Partial, + overrideRequest: Partial = {}, +): RelayRequest { + return { + relayData: { ...req.relayData, ...overrideData }, + request: { ...req.request, ...overrideRequest }, + }; +} + +export async function calculatePostGas( + token: any, + paymaster: any, + paymasterData: string, + account: Signer, + tokenAmount: BigNumber, + context: PrefixedHexString, +): Promise { + const calc = (await deployTestHub(true, account)) as TokenGasCalculator; + await paymaster.connect(account).setRelayHub(calc.address); + await token.connect(account).transfer(paymaster.address, tokenAmount); + const res = await calc.calculatePostGas(paymaster.address, context, paymasterData); + const receipt = await res.wait(); + const event = receipt.events?.find(it => it.event === "GasUsed") as unknown as GasUsedEvent; + return event.args.gasUsedByPost; +} + +export async function deployHub( + stakeManager: string, + penalizer: string, + batchGateway: string, + testToken: string, + testTokenMinimumStake: IntString, + signer: Signer, + configOverride: Partial = {}, + environment: Environment = defaultEnvironment, + relayRegistrationMaxAge = constants.yearInSec, +): Promise { + const relayHubConfiguration: RelayHubConfiguration = { + ...environment.relayHubConfiguration, + ...configOverride, + }; + const relayRegistrar = await new RelayRegistrar__factory(signer).deploy(relayRegistrationMaxAge); + const hub: RelayHub = await new RelayHub__factory(signer).deploy( + stakeManager, + penalizer, + batchGateway, + relayRegistrar.address, + relayHubConfiguration, + ); + + await hub.connect(signer).setMinimumStakes([testToken], [testTokenMinimumStake]); + + // @ts-ignore + hub._secretRegistrarInstance = relayRegistrar; + return hub; +} From e39c66917b4145fbd5f726d44a8c3e9d450cfa92 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Wed, 12 Oct 2022 07:05:04 -0400 Subject: [PATCH 23/30] feat(interface): ierc20 permit interface --- .../opty/IERC20PermitLegacy-0.8.x.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 contracts/interfaces/opty/IERC20PermitLegacy-0.8.x.sol diff --git a/contracts/interfaces/opty/IERC20PermitLegacy-0.8.x.sol b/contracts/interfaces/opty/IERC20PermitLegacy-0.8.x.sol new file mode 100644 index 000000000..1a3d8acbb --- /dev/null +++ b/contracts/interfaces/opty/IERC20PermitLegacy-0.8.x.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title Legacy ERC20Permit interface + * @author OptyFi + */ +interface IERC20PermitLegacy { + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} From c8b798b762e7f934f57c9c6a964c9797bcc1d1de Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Wed, 12 Oct 2022 07:06:51 -0400 Subject: [PATCH 24/30] feat(packages): add gsn --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 00cbe95ef..c877264f5 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,11 @@ }, "dependencies": { "@chainlink/contracts": "^0.3.1", - "@opengsn/contracts": "^3.0.0-beta.1", + "@opengsn/cli": "^3.0.0-beta.2", + "@opengsn/common": "^3.0.0-beta.2", + "@opengsn/contracts": "^3.0.0-beta.2", + "@opengsn/dev": "^3.0.0-beta.2", + "@opengsn/provider": "^3.0.0-beta.2", "@openzeppelin/contracts": "3.4.0", "@openzeppelin/contracts-0.8.x": "npm:@openzeppelin/contracts@4.4.1", "@optyfi/defi-legos": "v0.1.0-rc.38", From 435a19c2ce3bec92ab445ee3e8ac19239c1a815f Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Wed, 12 Oct 2022 07:13:31 -0400 Subject: [PATCH 25/30] feat(meta-tx-test): wip --- test/test-opty/meta-vault.spec.ts | 103 +++++- test/test-opty/token-paymaster.spec.ts | 460 +++++++++++++++++++++++++ 2 files changed, 551 insertions(+), 12 deletions(-) create mode 100644 test/test-opty/token-paymaster.spec.ts diff --git a/test/test-opty/meta-vault.spec.ts b/test/test-opty/meta-vault.spec.ts index 8bb96c60a..4af8a7c81 100644 --- a/test/test-opty/meta-vault.spec.ts +++ b/test/test-opty/meta-vault.spec.ts @@ -2,22 +2,26 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; import chai, { expect } from "chai"; import { solidity } from "ethereum-waffle"; import { BigNumber } from "ethers"; -import { deployments, ethers } from "hardhat"; +import { deployments, ethers, getChainId } from "hardhat"; import { eEVMNetwork } from "../../helper-hardhat-config"; import { MULTI_CHAIN_VAULT_TOKENS } from "../../helpers/constants/tokens"; import { getAccountsMerkleProof, getAccountsMerkleRoot, Signers, to_10powNumber_BN } from "../../helpers/utils"; -import { signMetaTxRequest } from "./utils"; +import { buildTypedData, signTypedData } from "./utils"; import { ERC20Permit, ERC20Permit__factory, + Forwarder, MetaVault, - MinimalForwarder, Registry, Registry__factory, Vault, Vault__factory, } from "../../typechain"; import { getPermitSignature, setTokenBalanceInStorage } from "./utils"; +import { GsnDomainSeparatorType } from "@opengsn/common"; +import { defaultGsnConfig } from "@opengsn/provider"; +import { SignTypedDataVersion, TypedDataUtils } from "@metamask/eth-sig-util"; +import keccak256 from "keccak256"; chai.use(solidity); const fork = process.env.FORK as eEVMNetwork; @@ -31,10 +35,12 @@ describe("MetaVault", function () { before(async function () { //deploy contracts await deployments.fixture(); - this.forwarder = await deploy("MinimalForwarder"); + this.forwarder = await deploy("Forwarder"); + await this.forwarder.registerRequestType("RelayRequest", ""); + await this.forwarder.registerDomainSeparator(defaultGsnConfig.domainSeparatorName, GsnDomainSeparatorType.version); this.metaVault = await deploy("MetaVault", this.forwarder.address); - const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get("opUSDCearn")).address; + const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get("opUSDC-Save")).address; const REGISTRY_PROXY_ADDRESS = (await deployments.get("RegistryProxy")).address; this.vault = await ethers.getContractAt(Vault__factory.abi, OPUSDCGROW_VAULT_ADDRESS); @@ -121,13 +127,86 @@ describe("MetaVault", function () { this.accountsProof, ]); - const { request, signature } = await signMetaTxRequest(this.signers.alice.provider, forwarder, { - from: this.signers.alice.address, - to: this.metaVault.address, - data: dataCall, - }); - - await expect(forwarder.execute(request, signature)) + // const { signature, request } = await signMetaTxRequest(this.signers.alice.provider, forwarder, { + // from: this.signers.alice.address, + // to: this.metaVault.address, + // data: dataCall, + // }); + + const provider = ethers.getDefaultProvider(); + const feeData = await provider.getFeeData(); + const gasPrice = feeData.gasPrice ? feeData.gasPrice.toString() : ""; + const relayRequest = { + request: { + from: this.signers.alice.address, + to: this.metaVault.address, + value: "0", + gas: "1000000", + nonce: (await forwarder.getNonce(this.signers.alice.address)).toString(), + data: dataCall, + validUntilTime: ethers.constants.MaxUint256.toString(), + }, + relayData: { + maxFeePerGas: gasPrice, + maxPriorityFeePerGas: gasPrice, + transactionCalldataGasUsed: "0", + relayWorker: ethers.constants.AddressZero, + paymaster: ethers.constants.AddressZero, + forwarder: forwarder.address, + paymasterData: "0x", + clientId: "1", + }, + }; + + const chainId = +(await getChainId()); + + // const typeData = new TypedRequestData( + // defaultGsnConfig.domainSeparatorName, + // chainId, + // forwarder.address, + // relayRequest + // ); + const domain = { + name: "GSN Relayed Transaction", + version: 3, + chainId, + verifyingContract: forwarder.address, + }; + + const dataToSign = await buildTypedData(forwarder, relayRequest.request); + const signature = await signTypedData(this.signers.alice.provider, this.signers.alice.address, dataToSign); + + const EIP712Domain = [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ]; + + const RelayRequest = [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "gas", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "data", type: "bytes" }, + { name: "validUntilTime", type: "uint256" }, + ]; + + const types = { + EIP712Domain, + RelayRequest, + }; + // console.log(typeData); + const GENERIC_PARAMS = + "address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime"; + const typeName = + "RelayRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime,RelayData(uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,uint256 transactionCalldataGasUsed,address relayWorker,address paymaster,address forwarder,bytes paymasterData,uint256 clientId)"; + const typeHash = keccak256(typeName); + // const domain: any = typeData.domain; + const domainSeparator = TypedDataUtils.hashStruct("EIP712Domain", domain, types, SignTypedDataVersion.V4); + + await expect(forwarder.execute(relayRequest.request, domainSeparator, typeHash, "0x", signature)) .to.emit(this.vault, "Transfer") .withArgs(ethers.constants.AddressZero, this.signers.alice.address, this.depositAmountUSDC); }); diff --git a/test/test-opty/token-paymaster.spec.ts b/test/test-opty/token-paymaster.spec.ts new file mode 100644 index 000000000..1c83fc28e --- /dev/null +++ b/test/test-opty/token-paymaster.spec.ts @@ -0,0 +1,460 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; +import chai, { expect } from "chai"; +import { solidity } from "ethereum-waffle"; +import { BigNumber, BytesLike } from "ethers"; +import { deployments, ethers, getChainId, network } from "hardhat"; +import { eEVMNetwork } from "../../helper-hardhat-config"; +import { getAccountsMerkleProof, getAccountsMerkleRoot, Signers, to_10powNumber_BN } from "../../helpers/utils"; +import { signTypedData } from "./utils"; +import { + ERC20Permit, + ERC20Permit__factory, + MetaVault, + Registry, + Registry__factory, + Vault, + Vault__factory, + TokenPaymaster, + RelayHub, + IUniswapV2Router02, + StakeManager, + IForwarder, +} from "../../typechain"; +import { getPermitSignature, setTokenBalanceInStorage } from "./utils"; +import { GsnTestEnvironment } from "@opengsn/cli/dist/GsnTestEnvironment"; +import { RelayRequest, cloneRelayRequest, defaultEnvironment, decodeRevertReason, constants } from "@opengsn/common"; +import { calculatePostGas, registerAsRelayServer, deployHub } from "./utils"; +import { Penalizer } from "../../typechain/Penalizer"; +import { defaultGsnConfig } from "@opengsn/provider"; +import { parseUnits } from "ethers/lib/utils"; +import { TypedRequestData, GsnDomainSeparatorType, GsnRequestType } from "@opengsn/common/dist/EIP712/TypedRequestData"; + +chai.use(solidity); +const fork = process.env.FORK as eEVMNetwork; + +async function deploy(name: string, ...params: any[]) { + const Contract = await ethers.getContractFactory(name); + return await Contract.deploy(...params).then(f => f.deployed()); +} + +describe("TokenPaymaster", function () { + let owner: SignerWithAddress; + let maker: SignerWithAddress; + let paymaster: TokenPaymaster; + let uniRouter: IUniswapV2Router02; + let relayHub: RelayHub; + let forwarder: IForwarder; + let usdc: ERC20Permit; + let metaVault: MetaVault; + let vault: Vault; + let registry: Registry; + + let stakeManager: StakeManager; + let penalizer: Penalizer; + let relayRequest: RelayRequest; + let paymasterData: string; + let metaTxSignature: BytesLike; + + before(async function () { + await deployments.fixture(); + const oracle = await deployments.get("OptyFiOracle"); + + [owner, maker] = await ethers.getSigners(); + stakeManager = ( + await deploy("StakeManager", defaultEnvironment.maxUnstakeDelay, 0, 0, owner.address, owner.address) + ); + penalizer = ( + await deploy( + "Penalizer", + defaultEnvironment.penalizerConfiguration.penalizeBlockDelay, + defaultEnvironment.penalizerConfiguration.penalizeBlockExpiration, + ) + ); + relayHub = ( + await deployHub( + stakeManager.address, + penalizer.address, + constants.ZERO_ADDRESS, + constants.ZERO_ADDRESS, + "0", + owner, + ) + ); + paymaster = await deploy("TokenPaymaster", oracle.address); + + const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; + const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; + const usdcSwapAmount = ethers.utils.parseUnits("10000", 6); + usdc = await ethers.getContractAt(ERC20Permit__factory.abi, USDC); + + const context = ethers.utils.defaultAbiCoder.encode( + ["address", "uint256", "address"], + [maker.address, 5000000, usdc.address], + ); + + // const USDCWhaleAddress = "0xee5b5b923ffce93a870b3104b7ca09c3db80047a"; + // await network.provider.request({ + // method: "hardhat_impersonateAccount", + // params: [USDCWhaleAddress], + // }); + // const USDCWhale = await ethers.getSigner(ethers.utils.getAddress(USDCWhaleAddress)); + + // //provide USDC whale with ETH to make required transactions + // let tx = maker.sendTransaction({ + // to: USDCWhaleAddress, + // value: ethers.utils.parseEther("1.0"), + // gasLimit: 10000000, + // }); + // (await tx).wait(); + + await setTokenBalanceInStorage(usdc, owner.address, "20000"); + + // tx = usdc.connect(USDCWhale).transfer(owner.address, ethers.utils.parseUnits("1000000", 6)); + // (await tx).wait(); + + const UniswapV2Router02Address = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"; + uniRouter = await ethers.getContractAt("IUniswapV2Router02", UniswapV2Router02Address); + + const swapDeadline = BigNumber.from("1000000000000000000000000000000000000"); + + const approveData = ethers.utils.defaultAbiCoder.encode( + ["address", "uint256"], + [uniRouter.address, usdcSwapAmount], + ); + paymasterData = ethers.utils.defaultAbiCoder.encode( + ["tuple(address, address, bool, bytes, bytes, bytes, address[], uint256)"], + [[usdc.address, uniRouter.address, false, "0x", approveData, "0x", [USDC, WETH], swapDeadline]], + ); + const gasUsedByPost = await calculatePostGas(usdc, paymaster, paymasterData, owner, usdcSwapAmount, context); + + await paymaster.connect(owner).setPostGasUsage(gasUsedByPost); + await paymaster.connect(owner).setRelayHub(relayHub.address); + + console.log("paymaster post with precharge=", (await paymaster.gasUsedByPost()).toString()); + forwarder = await deploy("Forwarder"); + metaVault = await deploy("MetaVault", forwarder.address); + await paymaster.setTrustedForwarder(forwarder.address); + await forwarder.registerRequestType(GsnRequestType.typeName, GsnRequestType.typeSuffix); + await forwarder.registerDomainSeparator(defaultGsnConfig.domainSeparatorName, GsnDomainSeparatorType.version); + + const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get("opUSDC-Save")).address; + const REGISTRY_PROXY_ADDRESS = (await deployments.get("RegistryProxy")).address; + + vault = await ethers.getContractAt(Vault__factory.abi, OPUSDCGROW_VAULT_ADDRESS); + registry = await ethers.getContractAt(Registry__factory.abi, REGISTRY_PROXY_ADDRESS); + + //Get signers + const financeOperatorAddress = await registry.getFinanceOperator(); + const governanceAddress = await registry.getGovernance(); + const signers: SignerWithAddress[] = await ethers.getSigners(); + this.signers = {} as Signers; + this.signers.financeOperator = await ethers.getSigner(financeOperatorAddress); + this.signers.governance = await ethers.getSigner(governanceAddress); + this.signers.alice = signers[3]; + // this.relayer = signers[4]; + + await setTokenBalanceInStorage(usdc, this.signers.alice.address, "40000"); + //set vault config + const _vaultConfiguration = BigNumber.from( + "908337486804231642580332837833655270430560746049134246454386846501909299200", + ); + await vault.connect(this.signers.governance).setVaultConfiguration(_vaultConfiguration); + await vault.connect(this.signers.financeOperator).setValueControlParams( + "10000000000", // 10,000 USDC + "1000000000", // 1000 USDC + "1000000000000", // 1,000,000 USDC + ); + + const _accountsRoot = getAccountsMerkleRoot([this.signers.alice.address, metaVault.address]); + await vault.connect(this.signers.governance).setWhitelistedAccountsRoot(_accountsRoot); + + let accountsProof = getAccountsMerkleProof([this.signers.alice.address, metaVault.address], metaVault.address); + + let depositAmountUSDC = BigNumber.from("1000").mul(to_10powNumber_BN("6")); + + const deadline = ethers.constants.MaxUint256; + const sig = await getPermitSignature(this.signers.alice, usdc, metaVault.address, depositAmountUSDC, deadline, { + version: "2", + }); + + let dataMetaVaultPermit = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"], + [this.signers.alice.address, metaVault.address, depositAmountUSDC, deadline, sig.v, sig.r, sig.s], + ); + + const dataCall = metaVault.interface.encodeFunctionData("deposit", [ + vault.address, + depositAmountUSDC, + BigNumber.from("0"), + dataMetaVaultPermit, + accountsProof, + ]); + + const provider = ethers.getDefaultProvider(); + const feeData = await provider.getFeeData(); + const gasPrice = feeData.gasPrice ? feeData.gasPrice.toString() : ""; + + relayRequest = { + request: { + from: this.signers.alice.address, + to: metaVault.address, + value: "0", + gas: "1000000", + nonce: (await forwarder.getNonce(this.signers.alice.address)).toString(), + data: dataCall, + validUntilTime: ethers.constants.MaxUint256.toString(), + }, + relayData: { + maxFeePerGas: gasPrice, + maxPriorityFeePerGas: gasPrice, + transactionCalldataGasUsed: "0", + relayWorker: maker.address, + paymaster: paymaster.address, + forwarder: forwarder.address, + paymasterData, + clientId: "1", + }, + }; + + const chainId = +(await getChainId()); //defaultEnvironment.chainId; + const dataToSign = new TypedRequestData( + defaultGsnConfig.domainSeparatorName, + chainId, + forwarder.address, + relayRequest, + ); + + metaTxSignature = await signTypedData(this.signers.alice.provider, this.signers.alice.address, dataToSign); + // metaTxSignature = await this.signers.alice._signTypedData( + // { + // name: 'GSN Relayed Transaction', + // version: '3', + // chainId: 1, + // verifyingContract: '0x1E49A8FC74EDfB7a0dc7F156B5415C2dE9eF9e98' + // }, + // { + // RelayRequest: [ + // { name: "from", type: "address" }, + // { name: "to", type: "address" }, + // { name: "value", type: "uint256" }, + // { name: "gas", type: "uint256" }, + // { name: "nonce", type: "uint256" }, + // { name: "data", type: "bytes" }, + // { name: "validUntilTime", type: "uint256" }, + // ], + // RelayData: [ + // { name: "maxFeePerGas", type: "uint256" }, + // { name: "maxPriorityFeePerGas", type: "uint256" }, + // { name: "transactionCalldataGasUsed", type: "uint256" }, + // { name: "relayWorker", type: "address" }, + // { name: "paymaster", type: "address" }, + // { name: "forwarder", type: "address" }, + // { name: "paymasterData", type: "bytes" }, + // { name: "clientId", type: "uint256" } + // ] + // }, + // { + // RelayRequest: { + // from: this.signers.alice.address, + // to: metaVault.address, + // value: "0", + // gas: "1000000", + // nonce: (await forwarder.getNonce(this.signers.alice.address)).toString(), + // data: dataCall, + // validUntilTime: ethers.constants.MaxUint256.toString(), + // }, + // RelayData: { + // maxFeePerGas: gasPrice, + // maxPriorityFeePerGas: gasPrice, + // transactionCalldataGasUsed: "0", + // relayWorker: maker.address, + // paymaster: paymaster.address, + // forwarder: forwarder.address, + // paymasterData, + // clientId: "1", + // } + // }, + // ); + //await signTypedData(this.signers.alice.provider, this.signers.alice.address, dataToSign); + // const signature = await signTypedData(this.signers.alice.provider, this.signers.alice.address, dataToSign); + // const { signature } = await signMetaTxRequest(this.signers.alice.provider, forwarder, relayRequest.request); + + // metaTxSignature = await signature; + }); + + after(async function () { + await GsnTestEnvironment.stopGsn(); + }); + + // describe("#preRelayedCall", function () { + // let testHub: TestHub + // describe("reverts if", function () { + // before(async function () { + // testHub = await deployTestHub(false, owner) as TestHub + // await paymaster.setRelayHub(testHub.address) + // }) + + // it("should reject if not enough balance", async () => { + // const req = mergeRelayRequest(relayRequest, { paymasterData: paymasterData }); + // console.log(req); + // await expect(testHub.callPreRC(req, metaTxSignature, '0x', 1e6)).to.revertedWith("ERC20: insufficient allowance"); + // }) + + // it("should reject if unknown paymasterData", async () => { + // const req = mergeRelayRequest(relayRequest, { paymasterData: '0x1234' }) + // const signature = await getEip712Signature(web3, new TypedRequestData(defaultGsnConfig.domainSeparatorName, 1, forwarder.address, req)) + // assert.match(await revertReason(testHub.callPreRC(req, signature, '0x', 1e6)), /paymasterData: invalid length for Uniswap v1 exchange address/) + // }) + + // it("should reject if unsupported uniswap in paymasterData", async () => { + // const req = mergeRelayRequest(relayRequest, { paymasterData: web3.eth.abi.encodeParameter('address', nonUniswap) }) + // const signature = await getEip712Signature(web3, new TypedRequestData(defaultGsnConfig.domainSeparatorName, 1, forwarder.address, req)) + // assert.match(await revertReason(testHub.callPreRC(req, signature, '0x', 1e6)), /unsupported token uniswap/) + // }) + // }) + + // context('with funded recipient', function () { + // before(async function () { + // await token.mint(5e18.toString()) + // await token.transfer(recipient.address, 5e18.toString()) + // }) + + // it('should reject if no token approval', async () => { + // const req = mergeRelayRequest(relayRequest, { paymasterData: web3.eth.abi.encodeParameter('address', uniswap.address) }) + // assert.match(await revertReason(testHub.callPreRC(req, signature, '0x', 1e6)), transferErc20Error) + // }) + + // context('with token approved for paymaster', function () { + // before(async function () { + // await recipient.execute(token.address, token.contract.methods.approve(paymaster.address, MAX_INTEGER.toString()).encodeABI()) + // }) + + // // deliberately removing this functionality as a bit redundant - just pass the token at all times + // it.skip('callPreRC should succeed and return default token/uniswap', async () => { + // const ret: any = await testHub.callPreRC.call(relayRequest, signature, '0x', 1e6) + // const decoded = web3.eth.abi.decodeParameters(['address', 'address', 'address', 'address'], ret.context) + // assert.equal(decoded[2], token.address) + // assert.equal(decoded[3], uniswap.address) + // }) + + // it('callPreRC should succeed with specific token/uniswap', async () => { + // const req = mergeRelayRequest(relayRequest, { paymasterData: web3.eth.abi.encodeParameter('address', uniswap.address) }) + // const signature = await getEip712Signature(web3, new TypedRequestData(defaultGsnConfig.domainSeparatorName, 1, forwarder.address, req)) + // const ret: any = await testHub.callPreRC.call(req, signature, '0x', 1e6) + // const decoded = web3.eth.abi.decodeParameters(['address', 'address', 'address', 'address'], ret.context) as any + // assert.equal(decoded[2], token.address) + // assert.equal(decoded[3], uniswap.address) + // }) + // }) + // }) + // }) + + describe("#relayedCall()", function () { + const paymasterDeposit = (1e18).toString(); + + before(async () => { + // TODO: not needed. use startGsn instead + // await GsnTestEnvironment.startGsn("mainnet"); + await setTokenBalanceInStorage(usdc, owner.address, "40000"); + + const stake = parseUnits("10000", 6).toString(); + await usdc.connect(owner).approve(stakeManager.address, stake); + await registerAsRelayServer(usdc, stakeManager, maker, owner, stake, relayHub); + await relayHub.depositFor(paymaster.address, { value: paymasterDeposit }); + await paymaster.setRelayHub(relayHub.address); + }); + + // it('should reject if incorrect signature', async () => { + // const wrongSignature = await getEip712Signature( + // web3, + // new TypedRequestData( + // defaultGsnConfig.domainSeparatorName, + // 222, + // forwarder.address, + // relayRequest + // ) + // ) + // const gas = 5000000 + + // const req = mergeRelayRequest(relayRequest, { paymasterData: web3.eth.abi.encodeParameter('address', uniswap.address) }) + // const relayCall: any = await hub.relayCall.call(defaultGsnConfig.domainSeparatorName, 1e06, req, wrongSignature, '0x', { + // from: relay, + // gas + // }) + // assert.equal(decodeRevertReason(relayCall.returnValue), 'FWD: signature mismatch') + // }) + + it("should pay with token to make a call", async function () { + const _relayRequest: any = cloneRelayRequest(relayRequest); + _relayRequest.request.nonce = (await forwarder.getNonce(maker.address)).toString(); + _relayRequest.relayData.maxFeePerGas = (1e9).toString(); + _relayRequest.relayData.maxPriorityFeePerGas = (1e9).toString(); + _relayRequest.request.value = BigNumber.from("0").toString(); + _relayRequest.request.gas = BigNumber.from("1000000").toString(); + + // _relayRequest.relayData.paymasterData = web3.eth.abi.encodeParameter('address', uniswap.address) + + // note that by default, ganache is buggy: getChainId returns 1337 but on-chain "chainid" returns 1. + // only if we pass it "--chainId 1337" the above 2 return the same value... + const chainId = await getChainId(); + + // const dataToSign = new TypedRequestData( + // defaultGsnConfig.domainSeparatorName, + // chainId, + // forwarder.address, + // _relayRequest + // ) + // const signature = await getEip712Signature( + // web3, + // dataToSign + // ) + const preBalance = await relayHub.balanceOf(paymaster.address); + + await usdc.connect(this.signers.alice).approve(paymaster.address, ethers.constants.MaxUint256); + // console.log(defaultGsnConfig.domainSeparatorName); + // console.log(10e6.toString()); + // console.log(await _relayRequest); + // console.log(metaTxSignature); + + // const externalGasLimit = 5e6.toString(); + const relayCall = await relayHub + .connect(maker) + .relayCall(defaultGsnConfig.domainSeparatorName, (10e6).toString(), await _relayRequest, metaTxSignature, "0x"); + + const ret = await relayCall.wait(); + + const rejected = ret.events?.find(log => log.event === "TransactionRejectedByPaymaster"); + // @ts-ignore + if (rejected != null) { + `Rejected with reason: ${decodeRevertReason(rejected?.args?.reason) as string}`; + } + // const relayed = ret.events?.find(log => log.event === "TransactionRelayed"); + // @ts-ignore + // const events = await paymaster.getPastEvents(); + // const chargedEvent = events.find((e: any) => e.event === "TokensCharged"); + + // console.log({ relayed, chargedEvent }) + // @ts-ignore + // console.log("charged: ", relayed?.args.charge.toString()); + // @ts-ignore + // assert.equal(relayed!.args.status, 0) + // const postTokens = await usdc.balanceOf(this.signers.alice.address); + // const usedTokens = preTokens.sub(postTokens); + + // console.log("alice tokens balance change (used ETH): ", usedTokens.toString()); + // // @ts-ignore + // console.log("reported charged tokens in TokensCharged: ", chargedEvent.args.tokenActualCharge.toString()); + // @ts-ignore + // const expectedTokenCharge = await uniswap.getTokenToEthOutputPrice(chargedEvent.args.ethActualCharge) + // assert.closeTo(usedTokens.toNumber(), expectedTokenCharge.toNumber(), 1000) + // const postBalance = await hub.balanceOf(paymaster.address) + + // assert.ok(postBalance >= preBalance, + // `expected paymaster balance not to be reduced: pre=${preBalance.toString()} post=${postBalance.toString()}`) + // // TODO: add test for relayed.args.charge, once gasUsedWithoutPost parameter is fixed (currently, its too high, and Paymaster "charges" too much) + // const postPaymasterTokens = await token.balanceOf(paymaster.address) + // console.log('Paymaster "earned" tokens:', postPaymasterTokens.sub(prePaymasterTokens).toString()) + // console.log('Paymaster "earned" deposit on RelayHub:', postBalance.sub(preBalance).toString()) + }); + }); +}); From 78d98c5c32eb9f4bc076014dc8e15d50c757bdd4 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Wed, 12 Oct 2022 09:22:28 -0400 Subject: [PATCH 26/30] fix(vault-gateway-test): wip -test forwarder --- ...ta-vault.spec.ts => vault-gateway.spec.ts} | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) rename test/test-opty/{meta-vault.spec.ts => vault-gateway.spec.ts} (86%) diff --git a/test/test-opty/meta-vault.spec.ts b/test/test-opty/vault-gateway.spec.ts similarity index 86% rename from test/test-opty/meta-vault.spec.ts rename to test/test-opty/vault-gateway.spec.ts index 4af8a7c81..b45252857 100644 --- a/test/test-opty/meta-vault.spec.ts +++ b/test/test-opty/vault-gateway.spec.ts @@ -18,7 +18,7 @@ import { Vault__factory, } from "../../typechain"; import { getPermitSignature, setTokenBalanceInStorage } from "./utils"; -import { GsnDomainSeparatorType } from "@opengsn/common"; +import { GsnDomainSeparatorType, GsnRequestType } from "@opengsn/common"; import { defaultGsnConfig } from "@opengsn/provider"; import { SignTypedDataVersion, TypedDataUtils } from "@metamask/eth-sig-util"; import keccak256 from "keccak256"; @@ -36,9 +36,10 @@ describe("MetaVault", function () { //deploy contracts await deployments.fixture(); this.forwarder = await deploy("Forwarder"); - await this.forwarder.registerRequestType("RelayRequest", ""); + await this.forwarder.registerRequestType(GsnRequestType.typeName, ""); + console.log(GsnRequestType.typeName); await this.forwarder.registerDomainSeparator(defaultGsnConfig.domainSeparatorName, GsnDomainSeparatorType.version); - this.metaVault = await deploy("MetaVault", this.forwarder.address); + this.metaVault = await deploy("VaultGateway", this.forwarder.address); const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get("opUSDC-Save")).address; const REGISTRY_PROXY_ADDRESS = (await deployments.get("RegistryProxy")).address; @@ -166,12 +167,6 @@ describe("MetaVault", function () { // forwarder.address, // relayRequest // ); - const domain = { - name: "GSN Relayed Transaction", - version: 3, - chainId, - verifyingContract: forwarder.address, - }; const dataToSign = await buildTypedData(forwarder, relayRequest.request); const signature = await signTypedData(this.signers.alice.provider, this.signers.alice.address, dataToSign); @@ -193,18 +188,36 @@ describe("MetaVault", function () { { name: "validUntilTime", type: "uint256" }, ]; - const types = { - EIP712Domain, - RelayRequest, + const data = { + domain: { + name: "GSN Relayed Transaction", + version: "3", + chainId, + verifyingContract: forwarder.address, + }, + primaryType: "ForwardRequest", + types: { + EIP712Domain: EIP712Domain, + ForwardRequest: RelayRequest, + }, + message: {}, }; + // console.log(typeData); const GENERIC_PARAMS = "address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime"; - const typeName = - "RelayRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime,RelayData(uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,uint256 transactionCalldataGasUsed,address relayWorker,address paymaster,address forwarder,bytes paymasterData,uint256 clientId)"; + // const typeName = + // "RelayRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime,RelayData(uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,uint256 transactionCalldataGasUsed,address relayWorker,address paymaster,address forwarder,bytes paymasterData,uint256 clientId)"; + // const typeHash = keccak256(typeName); + const typeName = `ForwardRequest(${GENERIC_PARAMS})`; const typeHash = keccak256(typeName); - // const domain: any = typeData.domain; - const domainSeparator = TypedDataUtils.hashStruct("EIP712Domain", domain, types, SignTypedDataVersion.V4); + + const domainSeparator = TypedDataUtils.hashStruct( + "EIP712Domain", + data.domain, + data.types, + SignTypedDataVersion.V4, + ); await expect(forwarder.execute(relayRequest.request, domainSeparator, typeHash, "0x", signature)) .to.emit(this.vault, "Transfer") From f3cb046f7cf8f175869db9321c05f4c33549895e Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Thu, 13 Oct 2022 09:46:58 -0400 Subject: [PATCH 27/30] fix(paymaster): fix gas and approvals --- contracts/protocol/meta-tx/GSN/BasePaymaster.sol | 7 +++---- contracts/protocol/meta-tx/TokenPaymaster.sol | 15 +++++---------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/contracts/protocol/meta-tx/GSN/BasePaymaster.sol b/contracts/protocol/meta-tx/GSN/BasePaymaster.sol index b5fbe9449..491f49cfe 100644 --- a/contracts/protocol/meta-tx/GSN/BasePaymaster.sol +++ b/contracts/protocol/meta-tx/GSN/BasePaymaster.sol @@ -30,11 +30,11 @@ abstract contract BasePaymaster is IPaymaster, Ownable, ERC165 { } //overhead of forwarder verify+signature, plus hub overhead. - uint256 public constant FORWARDER_HUB_OVERHEAD = 50000; + uint256 public constant FORWARDER_HUB_OVERHEAD = 500000; //These parameters are documented in IPaymaster.GasAndDataLimits - uint256 public constant PRE_RELAYED_CALL_GAS_LIMIT = 100000; - uint256 public constant POST_RELAYED_CALL_GAS_LIMIT = 110000; + uint256 public constant PRE_RELAYED_CALL_GAS_LIMIT = 1000000; + uint256 public constant POST_RELAYED_CALL_GAS_LIMIT = 1100000; uint256 public constant PAYMASTER_ACCEPTANCE_BUDGET = PRE_RELAYED_CALL_GAS_LIMIT + FORWARDER_HUB_OVERHEAD; uint256 public constant CALLDATA_SIZE_LIMIT = 10500; @@ -133,7 +133,6 @@ abstract contract BasePaymaster is IPaymaster, Ownable, ERC165 { _verifyForwarder(relayRequest); _verifyValue(relayRequest); _verifyPaymasterData(relayRequest); - _verifyApprovalData(approvalData); return _preRelayedCall(relayRequest, signature, approvalData, maxPossibleGas); } diff --git a/contracts/protocol/meta-tx/TokenPaymaster.sol b/contracts/protocol/meta-tx/TokenPaymaster.sol index 0cd9d3114..96abe53ac 100644 --- a/contracts/protocol/meta-tx/TokenPaymaster.sol +++ b/contracts/protocol/meta-tx/TokenPaymaster.sol @@ -3,13 +3,11 @@ pragma solidity ^0.8.0; pragma experimental ABIEncoderV2; //contracts -import { Ownable } from "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; import { ERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/ERC20.sol"; import { BasePaymaster } from "./GSN/BasePaymaster.sol"; //libraries import { SafeMath } from "@openzeppelin/contracts-0.8.x/utils/math/SafeMath.sol"; -import { SafeERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; import { GsnTypes } from "./GSN/libraries/GsnTypes.sol"; //interfaces @@ -17,7 +15,6 @@ import { IOptyFiOracle } from "../optyfi-oracle/contracts/interfaces/IOptyFiOrac import { IERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; import { IERC20Permit } from "@openzeppelin/contracts-0.8.x/token/ERC20/extensions/draft-IERC20Permit.sol"; import { IERC20PermitLegacy } from "../../interfaces/opty/IERC20PermitLegacy-0.8.x.sol"; -import { IForwarder } from "./interfaces/IForwarder.sol"; import { IUniswapV2Router01 } from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router01.sol"; import { ISwapRouter } from "../../interfaces/uniswap/ISwapRouter.sol"; @@ -29,7 +26,6 @@ import { ISwapRouter } from "../../interfaces/uniswap/ISwapRouter.sol"; * - postRelayedCall - refund the caller for the unused gas */ contract TokenPaymaster is BasePaymaster { - // using SafeERC20 for IERC20; using SafeMath for uint256; /** @dev ETH address */ @@ -133,11 +129,11 @@ contract TokenPaymaster is BasePaymaster { bytes calldata _approvalData, uint256 _maxPossibleGas ) internal virtual override returns (bytes memory context, bool revertOnRecipientRevert) { - (_signature, _approvalData); + (_signature); IERC20 token = _getToken(_relayRequest.relayData.paymasterData); + _permit(address(token), _approvalData); (address payer, uint256 tokenPrecharge) = _calculatePreCharge(address(token), _relayRequest, _maxPossibleGas); bool success = token.transferFrom(payer, address(this), tokenPrecharge); - uint256 x = 10; return (abi.encode(payer, tokenPrecharge, token), false); } @@ -176,7 +172,6 @@ contract TokenPaymaster is BasePaymaster { ) internal { uint256 ethActualCharge = relayHub.calculateCharge(_gasUseWithoutPost.add(gasUsedByPost), _relayData); uint256 tokenActualCharge = _getETHInToken(address(_token), _valueRequested.add(ethActualCharge)); - uint256 tokenRefund = _tokenPrecharge.sub(tokenActualCharge); _refundPayer(_payer, _token, tokenRefund); _depositProceedsToHub(_payer, ethActualCharge, _relayData.paymasterData); @@ -225,7 +220,7 @@ contract TokenPaymaster is BasePaymaster { PaymasterData memory pd = _getPaymasterData(_paymasterData); _approve(pd.token, pd.approve); _permit(pd.token, pd.permit); - uint256[] memory amounts; + uint256 balanceBefore = address(this).balance; if (pd.isUniV3) { ISwapRouter(pd.dex).exactInput( ISwapRouter.ExactInputParams({ @@ -237,7 +232,7 @@ contract TokenPaymaster is BasePaymaster { }) ); } else { - amounts = IUniswapV2Router01(pd.dex).swapExactTokensForETH( + IUniswapV2Router01(pd.dex).swapExactTokensForETH( _getETHInToken(pd.token, _ethActualCharge).mul(101).div(100), 0, //_ethActualCharge, pd.pathUniV2, @@ -245,6 +240,7 @@ contract TokenPaymaster is BasePaymaster { pd.deadline ); } + receivedAmount = address(this).balance.sub(balanceBefore); } /** @@ -263,7 +259,6 @@ contract TokenPaymaster is BasePaymaster { require(price > uint256(0), "!price"); uint256 decimals0 = ERC20(_token0).decimals(); uint256 decimals1 = ERC20(_token1).decimals(); - _swapOutAmount = ((_swapInAmount * price * 10**decimals1) / 10**(18 + decimals0)); } From b53a1275033f7e9b9149ea886436be5ec74d8210 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Thu, 13 Oct 2022 09:48:21 -0400 Subject: [PATCH 28/30] fix(utils): remove console log --- test/test-opty/utils.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/test-opty/utils.ts b/test/test-opty/utils.ts index dd512751c..9225c70a8 100644 --- a/test/test-opty/utils.ts +++ b/test/test-opty/utils.ts @@ -358,6 +358,7 @@ function getMetaTxTypeData(chainId: number, verifyingContract: string) { types: { EIP712Domain, RelayRequest, + RelayData, }, domain: { name: "GSN Relayed Transaction", @@ -467,23 +468,16 @@ export async function registerAsRelayServer( stake: string, hub: RelayHub, ): Promise { - console.log("1.1"); await stakeManager.connect(relay).setRelayManagerOwner(relayOwner.address); - console.log("1.2"); await stakeManager .connect(relayOwner) .stakeForRelayManager(token.address, relay.address, 7 * 24 * 3600, stake.toString()); - console.log("1.3"); await stakeManager.connect(relayOwner).authorizeHubByOwner(relay.address, hub.address); - console.log("1.4"); await hub.setMinimumStakes([token.address], [stake]); - console.log("1.5"); await hub.connect(relay).addRelayWorkers([relay.address]); - console.log("1.6"); const relayRegistrar = ( await hre.ethers.getContractAt("RelayRegistrar", await hub.getRelayRegistrar()) ); - console.log("1.7"); await relayRegistrar.connect(relay).registerRelayServer(hub.address, splitRelayUrlForRegistrar("url")); } From b509a150145278fad975d5f64abf303770abbb0d Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Thu, 13 Oct 2022 09:49:03 -0400 Subject: [PATCH 29/30] fix(tests): wip tests --- test/test-opty/token-paymaster.spec.ts | 132 +++++++----------------- test/test-opty/vault-gateway.spec.ts | 136 ++++++++++++++----------- 2 files changed, 117 insertions(+), 151 deletions(-) diff --git a/test/test-opty/token-paymaster.spec.ts b/test/test-opty/token-paymaster.spec.ts index 1c83fc28e..63a5f0cb8 100644 --- a/test/test-opty/token-paymaster.spec.ts +++ b/test/test-opty/token-paymaster.spec.ts @@ -9,7 +9,7 @@ import { signTypedData } from "./utils"; import { ERC20Permit, ERC20Permit__factory, - MetaVault, + VaultGateway, Registry, Registry__factory, Vault, @@ -45,15 +45,15 @@ describe("TokenPaymaster", function () { let relayHub: RelayHub; let forwarder: IForwarder; let usdc: ERC20Permit; - let metaVault: MetaVault; + let vaultGateway: VaultGateway; let vault: Vault; let registry: Registry; - let stakeManager: StakeManager; let penalizer: Penalizer; let relayRequest: RelayRequest; let paymasterData: string; let metaTxSignature: BytesLike; + let depositAmountUSDC: BigNumber; before(async function () { await deployments.fixture(); @@ -92,26 +92,8 @@ describe("TokenPaymaster", function () { [maker.address, 5000000, usdc.address], ); - // const USDCWhaleAddress = "0xee5b5b923ffce93a870b3104b7ca09c3db80047a"; - // await network.provider.request({ - // method: "hardhat_impersonateAccount", - // params: [USDCWhaleAddress], - // }); - // const USDCWhale = await ethers.getSigner(ethers.utils.getAddress(USDCWhaleAddress)); - - // //provide USDC whale with ETH to make required transactions - // let tx = maker.sendTransaction({ - // to: USDCWhaleAddress, - // value: ethers.utils.parseEther("1.0"), - // gasLimit: 10000000, - // }); - // (await tx).wait(); - await setTokenBalanceInStorage(usdc, owner.address, "20000"); - // tx = usdc.connect(USDCWhale).transfer(owner.address, ethers.utils.parseUnits("1000000", 6)); - // (await tx).wait(); - const UniswapV2Router02Address = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"; uniRouter = await ethers.getContractAt("IUniswapV2Router02", UniswapV2Router02Address); @@ -132,7 +114,7 @@ describe("TokenPaymaster", function () { console.log("paymaster post with precharge=", (await paymaster.gasUsedByPost()).toString()); forwarder = await deploy("Forwarder"); - metaVault = await deploy("MetaVault", forwarder.address); + vaultGateway = await deploy("VaultGateway", forwarder.address); await paymaster.setTrustedForwarder(forwarder.address); await forwarder.registerRequestType(GsnRequestType.typeName, GsnRequestType.typeSuffix); await forwarder.registerDomainSeparator(defaultGsnConfig.domainSeparatorName, GsnDomainSeparatorType.version); @@ -165,24 +147,28 @@ describe("TokenPaymaster", function () { "1000000000000", // 1,000,000 USDC ); - const _accountsRoot = getAccountsMerkleRoot([this.signers.alice.address, metaVault.address]); + const _accountsRoot = getAccountsMerkleRoot([this.signers.alice.address, vaultGateway.address]); await vault.connect(this.signers.governance).setWhitelistedAccountsRoot(_accountsRoot); - let accountsProof = getAccountsMerkleProof([this.signers.alice.address, metaVault.address], metaVault.address); + let accountsProof = getAccountsMerkleProof( + [this.signers.alice.address, vaultGateway.address], + vaultGateway.address, + ); - let depositAmountUSDC = BigNumber.from("1000").mul(to_10powNumber_BN("6")); + depositAmountUSDC = BigNumber.from("1000").mul(to_10powNumber_BN("6")); const deadline = ethers.constants.MaxUint256; - const sig = await getPermitSignature(this.signers.alice, usdc, metaVault.address, depositAmountUSDC, deadline, { + const sig = await getPermitSignature(this.signers.alice, usdc, vaultGateway.address, depositAmountUSDC, deadline, { version: "2", + nonce: BigNumber.from("1"), }); let dataMetaVaultPermit = ethers.utils.defaultAbiCoder.encode( ["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"], - [this.signers.alice.address, metaVault.address, depositAmountUSDC, deadline, sig.v, sig.r, sig.s], + [this.signers.alice.address, vaultGateway.address, depositAmountUSDC, deadline, sig.v, sig.r, sig.s], ); - const dataCall = metaVault.interface.encodeFunctionData("deposit", [ + const dataCall = vaultGateway.interface.encodeFunctionData("deposit", [ vault.address, depositAmountUSDC, BigNumber.from("0"), @@ -197,7 +183,7 @@ describe("TokenPaymaster", function () { relayRequest = { request: { from: this.signers.alice.address, - to: metaVault.address, + to: vaultGateway.address, value: "0", gas: "1000000", nonce: (await forwarder.getNonce(this.signers.alice.address)).toString(), @@ -225,61 +211,6 @@ describe("TokenPaymaster", function () { ); metaTxSignature = await signTypedData(this.signers.alice.provider, this.signers.alice.address, dataToSign); - // metaTxSignature = await this.signers.alice._signTypedData( - // { - // name: 'GSN Relayed Transaction', - // version: '3', - // chainId: 1, - // verifyingContract: '0x1E49A8FC74EDfB7a0dc7F156B5415C2dE9eF9e98' - // }, - // { - // RelayRequest: [ - // { name: "from", type: "address" }, - // { name: "to", type: "address" }, - // { name: "value", type: "uint256" }, - // { name: "gas", type: "uint256" }, - // { name: "nonce", type: "uint256" }, - // { name: "data", type: "bytes" }, - // { name: "validUntilTime", type: "uint256" }, - // ], - // RelayData: [ - // { name: "maxFeePerGas", type: "uint256" }, - // { name: "maxPriorityFeePerGas", type: "uint256" }, - // { name: "transactionCalldataGasUsed", type: "uint256" }, - // { name: "relayWorker", type: "address" }, - // { name: "paymaster", type: "address" }, - // { name: "forwarder", type: "address" }, - // { name: "paymasterData", type: "bytes" }, - // { name: "clientId", type: "uint256" } - // ] - // }, - // { - // RelayRequest: { - // from: this.signers.alice.address, - // to: metaVault.address, - // value: "0", - // gas: "1000000", - // nonce: (await forwarder.getNonce(this.signers.alice.address)).toString(), - // data: dataCall, - // validUntilTime: ethers.constants.MaxUint256.toString(), - // }, - // RelayData: { - // maxFeePerGas: gasPrice, - // maxPriorityFeePerGas: gasPrice, - // transactionCalldataGasUsed: "0", - // relayWorker: maker.address, - // paymaster: paymaster.address, - // forwarder: forwarder.address, - // paymasterData, - // clientId: "1", - // } - // }, - // ); - //await signTypedData(this.signers.alice.provider, this.signers.alice.address, dataToSign); - // const signature = await signTypedData(this.signers.alice.provider, this.signers.alice.address, dataToSign); - // const { signature } = await signMetaTxRequest(this.signers.alice.provider, forwarder, relayRequest.request); - - // metaTxSignature = await signature; }); after(async function () { @@ -385,12 +316,12 @@ describe("TokenPaymaster", function () { // }) it("should pay with token to make a call", async function () { - const _relayRequest: any = cloneRelayRequest(relayRequest); - _relayRequest.request.nonce = (await forwarder.getNonce(maker.address)).toString(); - _relayRequest.relayData.maxFeePerGas = (1e9).toString(); - _relayRequest.relayData.maxPriorityFeePerGas = (1e9).toString(); - _relayRequest.request.value = BigNumber.from("0").toString(); - _relayRequest.request.gas = BigNumber.from("1000000").toString(); + // const _relayRequest: any = cloneRelayRequest(relayRequest); + // _relayRequest.request.nonce = (await forwarder.getNonce(maker.address)).toString(); + // _relayRequest.relayData.maxFeePerGas = (1e9).toString(); + // _relayRequest.relayData.maxPriorityFeePerGas = (1e9).toString(); + // _relayRequest.request.value = BigNumber.from("0").toString(); + // _relayRequest.request.gas = BigNumber.from("1000000").toString(); // _relayRequest.relayData.paymasterData = web3.eth.abi.encodeParameter('address', uniswap.address) @@ -409,19 +340,34 @@ describe("TokenPaymaster", function () { // dataToSign // ) const preBalance = await relayHub.balanceOf(paymaster.address); + const deadline = ethers.constants.MaxUint256; + const sig = await getPermitSignature(this.signers.alice, usdc, paymaster.address, depositAmountUSDC, deadline, { + version: "2", + }); + + let permitData = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"], + [this.signers.alice.address, paymaster.address, depositAmountUSDC, deadline, sig.v, sig.r, sig.s], + ); - await usdc.connect(this.signers.alice).approve(paymaster.address, ethers.constants.MaxUint256); + // await usdc.connect(this.signers.alice).approve(paymaster.address, ethers.constants.MaxUint256); // console.log(defaultGsnConfig.domainSeparatorName); // console.log(10e6.toString()); // console.log(await _relayRequest); // console.log(metaTxSignature); + console.log("maker pre balance", await maker.getBalance()); + console.log("alice pre balance", await vault.balanceOf(this.signers.alice.address)); - // const externalGasLimit = 5e6.toString(); + const externalGasLimit = (5e6).toString(); const relayCall = await relayHub .connect(maker) - .relayCall(defaultGsnConfig.domainSeparatorName, (10e6).toString(), await _relayRequest, metaTxSignature, "0x"); + .relayCall(defaultGsnConfig.domainSeparatorName, (10e6).toString(), relayRequest, metaTxSignature, permitData, { + gasLimit: externalGasLimit, + }); const ret = await relayCall.wait(); + console.log("maker post balance", await maker.getBalance()); + console.log("alice post balance", await vault.balanceOf(this.signers.alice.address)); const rejected = ret.events?.find(log => log.event === "TransactionRejectedByPaymaster"); // @ts-ignore diff --git a/test/test-opty/vault-gateway.spec.ts b/test/test-opty/vault-gateway.spec.ts index b45252857..5727f668c 100644 --- a/test/test-opty/vault-gateway.spec.ts +++ b/test/test-opty/vault-gateway.spec.ts @@ -6,19 +6,19 @@ import { deployments, ethers, getChainId } from "hardhat"; import { eEVMNetwork } from "../../helper-hardhat-config"; import { MULTI_CHAIN_VAULT_TOKENS } from "../../helpers/constants/tokens"; import { getAccountsMerkleProof, getAccountsMerkleRoot, Signers, to_10powNumber_BN } from "../../helpers/utils"; -import { buildTypedData, signTypedData } from "./utils"; +import { signTypedData } from "./utils"; import { ERC20Permit, ERC20Permit__factory, Forwarder, - MetaVault, + VaultGateway, Registry, Registry__factory, Vault, Vault__factory, } from "../../typechain"; import { getPermitSignature, setTokenBalanceInStorage } from "./utils"; -import { GsnDomainSeparatorType, GsnRequestType } from "@opengsn/common"; +import { GsnDomainSeparatorType, GsnRequestType, TypedRequestData } from "@opengsn/common"; import { defaultGsnConfig } from "@opengsn/provider"; import { SignTypedDataVersion, TypedDataUtils } from "@metamask/eth-sig-util"; import keccak256 from "keccak256"; @@ -31,15 +31,43 @@ async function deploy(name: string, ...params: any[]) { return await Contract.deploy(...params).then(f => f.deployed()); } -describe("MetaVault", function () { +const EIP712Domain = [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, +]; + +const RelayRequest = [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "gas", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "data", type: "bytes" }, + { name: "validUntilTime", type: "uint256" }, + { name: "relayData", type: "RelayData" }, +]; + +const RelayData = [ + { name: "maxFeePerGas", type: "uint256" }, + { name: "maxPriorityFeePerGas", type: "uint256" }, + { name: "transactionCalldataGasUsed", type: "uint256" }, + { name: "relayWorker", type: "address" }, + { name: "paymaster", type: "address" }, + { name: "forwarder", type: "address" }, + { name: "paymasterData", type: "bytes" }, + { name: "clientId", type: "uint256" }, +]; + +describe("VaultGateway", function () { before(async function () { //deploy contracts await deployments.fixture(); this.forwarder = await deploy("Forwarder"); - await this.forwarder.registerRequestType(GsnRequestType.typeName, ""); - console.log(GsnRequestType.typeName); + await this.forwarder.registerRequestType(GsnRequestType.typeName, GsnRequestType.typeSuffix); await this.forwarder.registerDomainSeparator(defaultGsnConfig.domainSeparatorName, GsnDomainSeparatorType.version); - this.metaVault = await deploy("VaultGateway", this.forwarder.address); + this.vaultGateway = await deploy("VaultGateway", this.forwarder.address); const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get("opUSDC-Save")).address; const REGISTRY_PROXY_ADDRESS = (await deployments.get("RegistryProxy")).address; @@ -56,6 +84,10 @@ describe("MetaVault", function () { this.signers.governance = await ethers.getSigner(governanceAddress); this.signers.alice = signers[0]; this.relayer = signers[1]; + console.log("financeOperator", this.signers.financeOperator.address); + console.log("governance", this.signers.governance.address); + console.log("alice", this.signers.alice.address); + console.log("relayer", this.relayer.address); //set vault config const _vaultConfiguration = BigNumber.from( @@ -68,7 +100,7 @@ describe("MetaVault", function () { "1000000000000", // 1,000,000 USDC ); - const _accountsRoot = getAccountsMerkleRoot([this.signers.alice.address, this.metaVault.address]); + const _accountsRoot = getAccountsMerkleRoot([this.signers.alice.address, this.vaultGateway.address]); await this.vault.connect(this.signers.governance).setWhitelistedAccountsRoot(_accountsRoot); //add USDC balance to user @@ -78,32 +110,32 @@ describe("MetaVault", function () { await setTokenBalanceInStorage(this.usdc, this.signers.alice.address, "20000"); this.accountsProof = getAccountsMerkleProof( - [this.signers.alice.address, this.metaVault.address], - this.metaVault.address, + [this.signers.alice.address, this.vaultGateway.address], + this.vaultGateway.address, ); this.depositAmountUSDC = BigNumber.from("1000").mul(to_10powNumber_BN("6")); }); - describe("MetaVault Test", function () { + describe("VaultGateway Test", function () { beforeEach(async function () { const deadline = ethers.constants.MaxUint256; const sig = await getPermitSignature( this.signers.alice, this.usdc, - this.metaVault.address, + this.vaultGateway.address, this.depositAmountUSDC, deadline, { version: "2" }, ); this.dataMetaVaultPermit = ethers.utils.defaultAbiCoder.encode( ["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"], - [this.signers.alice.address, this.metaVault.address, this.depositAmountUSDC, deadline, sig.v, sig.r, sig.s], + [this.signers.alice.address, this.vaultGateway.address, this.depositAmountUSDC, deadline, sig.v, sig.r, sig.s], ); }); - it("deposit using MetaVaults directly", async function () { + it("deposit using VaultGateway directly", async function () { await expect( - this.metaVault + this.vaultGateway .connect(this.signers.alice) .deposit( this.vault.address, @@ -120,7 +152,7 @@ describe("MetaVault", function () { it("deposits via a meta-tx", async function () { const forwarder = this.forwarder.connect(this.relayer); - const dataCall = this.metaVault.interface.encodeFunctionData("deposit", [ + const dataCall = this.vaultGateway.interface.encodeFunctionData("deposit", [ this.vault.address, this.depositAmountUSDC, BigNumber.from("0"), @@ -128,19 +160,13 @@ describe("MetaVault", function () { this.accountsProof, ]); - // const { signature, request } = await signMetaTxRequest(this.signers.alice.provider, forwarder, { - // from: this.signers.alice.address, - // to: this.metaVault.address, - // data: dataCall, - // }); - const provider = ethers.getDefaultProvider(); const feeData = await provider.getFeeData(); const gasPrice = feeData.gasPrice ? feeData.gasPrice.toString() : ""; const relayRequest = { request: { from: this.signers.alice.address, - to: this.metaVault.address, + to: this.vaultGateway.address, value: "0", gas: "1000000", nonce: (await forwarder.getNonce(this.signers.alice.address)).toString(), @@ -161,32 +187,14 @@ describe("MetaVault", function () { const chainId = +(await getChainId()); - // const typeData = new TypedRequestData( - // defaultGsnConfig.domainSeparatorName, - // chainId, - // forwarder.address, - // relayRequest - // ); - - const dataToSign = await buildTypedData(forwarder, relayRequest.request); - const signature = await signTypedData(this.signers.alice.provider, this.signers.alice.address, dataToSign); - - const EIP712Domain = [ - { name: "name", type: "string" }, - { name: "version", type: "string" }, - { name: "chainId", type: "uint256" }, - { name: "verifyingContract", type: "address" }, - ]; - - const RelayRequest = [ - { name: "from", type: "address" }, - { name: "to", type: "address" }, - { name: "value", type: "uint256" }, - { name: "gas", type: "uint256" }, - { name: "nonce", type: "uint256" }, - { name: "data", type: "bytes" }, - { name: "validUntilTime", type: "uint256" }, - ]; + const typeData = new TypedRequestData( + defaultGsnConfig.domainSeparatorName, + chainId, + forwarder.address, + relayRequest, + ); + + const signature = await signTypedData(this.signers.alice.provider, this.signers.alice.address, typeData); const data = { domain: { @@ -195,23 +203,35 @@ describe("MetaVault", function () { chainId, verifyingContract: forwarder.address, }, - primaryType: "ForwardRequest", + primaryType: "RelayRequest", types: { EIP712Domain: EIP712Domain, - ForwardRequest: RelayRequest, + RelayRequest: RelayRequest, + RelayData: RelayData, + }, + message: typeData.message, + }; + + const data2 = { + types: { + RelayData: RelayData, }, - message: {}, }; - // console.log(typeData); const GENERIC_PARAMS = "address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime"; - // const typeName = - // "RelayRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime,RelayData(uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,uint256 transactionCalldataGasUsed,address relayWorker,address paymaster,address forwarder,bytes paymasterData,uint256 clientId)"; - // const typeHash = keccak256(typeName); - const typeName = `ForwardRequest(${GENERIC_PARAMS})`; + const typeSuffix = + "RelayData relayData)RelayData(uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,uint256 transactionCalldataGasUsed,address relayWorker,address paymaster,address forwarder,bytes paymasterData,uint256 clientId)"; + const typeName = `RelayRequest(${GENERIC_PARAMS},${typeSuffix}`; const typeHash = keccak256(typeName); + const hashSuffix = TypedDataUtils.hashStruct( + "RelayData", + relayRequest.relayData, + data2.types, + SignTypedDataVersion.V4, + ); + const domainSeparator = TypedDataUtils.hashStruct( "EIP712Domain", data.domain, @@ -219,7 +239,7 @@ describe("MetaVault", function () { SignTypedDataVersion.V4, ); - await expect(forwarder.execute(relayRequest.request, domainSeparator, typeHash, "0x", signature)) + await expect(forwarder.execute(relayRequest.request, domainSeparator, typeHash, hashSuffix, signature)) .to.emit(this.vault, "Transfer") .withArgs(ethers.constants.AddressZero, this.signers.alice.address, this.depositAmountUSDC); }); From 90e347e106a6bd863d9c35bdbff03adcddd056d9 Mon Sep 17 00:00:00 2001 From: albuquerque-rafael Date: Sat, 15 Oct 2022 22:45:07 -0400 Subject: [PATCH 30/30] fix(test): test --- test/test-opty/token-paymaster.spec.ts | 387 +++++++++---------------- test/test-opty/utils.ts | 29 +- 2 files changed, 169 insertions(+), 247 deletions(-) diff --git a/test/test-opty/token-paymaster.spec.ts b/test/test-opty/token-paymaster.spec.ts index 63a5f0cb8..6b3263838 100644 --- a/test/test-opty/token-paymaster.spec.ts +++ b/test/test-opty/token-paymaster.spec.ts @@ -5,24 +5,21 @@ import { BigNumber, BytesLike } from "ethers"; import { deployments, ethers, getChainId, network } from "hardhat"; import { eEVMNetwork } from "../../helper-hardhat-config"; import { getAccountsMerkleProof, getAccountsMerkleRoot, Signers, to_10powNumber_BN } from "../../helpers/utils"; -import { signTypedData } from "./utils"; +import { deployTestHub, getVaultDeploymentAndConfigure, signTypedData } from "./utils"; import { ERC20Permit, ERC20Permit__factory, VaultGateway, - Registry, - Registry__factory, Vault, - Vault__factory, TokenPaymaster, RelayHub, - IUniswapV2Router02, StakeManager, IForwarder, + TestHub, } from "../../typechain"; import { getPermitSignature, setTokenBalanceInStorage } from "./utils"; import { GsnTestEnvironment } from "@opengsn/cli/dist/GsnTestEnvironment"; -import { RelayRequest, cloneRelayRequest, defaultEnvironment, decodeRevertReason, constants } from "@opengsn/common"; +import { RelayRequest, defaultEnvironment, decodeRevertReason, constants } from "@opengsn/common"; import { calculatePostGas, registerAsRelayServer, deployHub } from "./utils"; import { Penalizer } from "../../typechain/Penalizer"; import { defaultGsnConfig } from "@opengsn/provider"; @@ -39,27 +36,35 @@ async function deploy(name: string, ...params: any[]) { describe("TokenPaymaster", function () { let owner: SignerWithAddress; - let maker: SignerWithAddress; + let relayer: SignerWithAddress; + let alice: SignerWithAddress; + let paymaster: TokenPaymaster; - let uniRouter: IUniswapV2Router02; let relayHub: RelayHub; let forwarder: IForwarder; let usdc: ERC20Permit; let vaultGateway: VaultGateway; let vault: Vault; - let registry: Registry; let stakeManager: StakeManager; let penalizer: Penalizer; let relayRequest: RelayRequest; let paymasterData: string; let metaTxSignature: BytesLike; let depositAmountUSDC: BigNumber; + let permitData: string; + + const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; + const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; + const UniswapV2Router02Address = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"; before(async function () { await deployments.fixture(); + [owner, relayer, alice] = await ethers.getSigners(); + + //Get Oracle deployment const oracle = await deployments.get("OptyFiOracle"); - [owner, maker] = await ethers.getSigners(); + //Deploy Relay Hub stakeManager = ( await deploy("StakeManager", defaultEnvironment.maxUnstakeDelay, 0, 0, owner.address, owner.address) ); @@ -80,113 +85,102 @@ describe("TokenPaymaster", function () { owner, ) ); + + //Deploy Forwarder and set request type and domain separator + forwarder = await deploy("Forwarder"); + await forwarder.registerRequestType(GsnRequestType.typeName, GsnRequestType.typeSuffix); + await forwarder.registerDomainSeparator(defaultGsnConfig.domainSeparatorName, GsnDomainSeparatorType.version); + + //Deploy Paymaster, set RelayHub and Forwarder paymaster = await deploy("TokenPaymaster", oracle.address); + await paymaster.connect(owner).setRelayHub(relayHub.address); + await paymaster.setTrustedForwarder(forwarder.address); + + //Deploy Vault Gateway + vaultGateway = await deploy("VaultGateway", forwarder.address); - const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; - const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; - const usdcSwapAmount = ethers.utils.parseUnits("10000", 6); + //Get USDC instance usdc = await ethers.getContractAt(ERC20Permit__factory.abi, USDC); + //Set balance for Owner and Alice + await setTokenBalanceInStorage(usdc, owner.address, "20000"); + await setTokenBalanceInStorage(usdc, alice.address, "40000"); + const context = ethers.utils.defaultAbiCoder.encode( ["address", "uint256", "address"], - [maker.address, 5000000, usdc.address], + [relayer.address, 5000000, usdc.address], ); - await setTokenBalanceInStorage(usdc, owner.address, "20000"); - - const UniswapV2Router02Address = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"; - uniRouter = await ethers.getContractAt("IUniswapV2Router02", UniswapV2Router02Address); + //Set deposit amount + depositAmountUSDC = ethers.utils.parseUnits("10000", 6); + //Set swap deadline const swapDeadline = BigNumber.from("1000000000000000000000000000000000000"); + //Encode dex approval and paymaster data (used to refund relayer) const approveData = ethers.utils.defaultAbiCoder.encode( ["address", "uint256"], - [uniRouter.address, usdcSwapAmount], + [UniswapV2Router02Address, depositAmountUSDC], ); paymasterData = ethers.utils.defaultAbiCoder.encode( ["tuple(address, address, bool, bytes, bytes, bytes, address[], uint256)"], - [[usdc.address, uniRouter.address, false, "0x", approveData, "0x", [USDC, WETH], swapDeadline]], + [[usdc.address, UniswapV2Router02Address, false, "0x", approveData, "0x", [USDC, WETH], swapDeadline]], ); - const gasUsedByPost = await calculatePostGas(usdc, paymaster, paymasterData, owner, usdcSwapAmount, context); + //Calculate gas spent in postRelayCall (swap tokens to refund relayer) + const gasUsedByPost = await calculatePostGas(usdc, paymaster, paymasterData, owner, depositAmountUSDC, context); + + //Set postRelayCall gas usage await paymaster.connect(owner).setPostGasUsage(gasUsedByPost); - await paymaster.connect(owner).setRelayHub(relayHub.address); console.log("paymaster post with precharge=", (await paymaster.gasUsedByPost()).toString()); - forwarder = await deploy("Forwarder"); - vaultGateway = await deploy("VaultGateway", forwarder.address); - await paymaster.setTrustedForwarder(forwarder.address); - await forwarder.registerRequestType(GsnRequestType.typeName, GsnRequestType.typeSuffix); - await forwarder.registerDomainSeparator(defaultGsnConfig.domainSeparatorName, GsnDomainSeparatorType.version); - const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get("opUSDC-Save")).address; - const REGISTRY_PROXY_ADDRESS = (await deployments.get("RegistryProxy")).address; - - vault = await ethers.getContractAt(Vault__factory.abi, OPUSDCGROW_VAULT_ADDRESS); - registry = await ethers.getContractAt(Registry__factory.abi, REGISTRY_PROXY_ADDRESS); - - //Get signers - const financeOperatorAddress = await registry.getFinanceOperator(); - const governanceAddress = await registry.getGovernance(); - const signers: SignerWithAddress[] = await ethers.getSigners(); - this.signers = {} as Signers; - this.signers.financeOperator = await ethers.getSigner(financeOperatorAddress); - this.signers.governance = await ethers.getSigner(governanceAddress); - this.signers.alice = signers[3]; - // this.relayer = signers[4]; - - await setTokenBalanceInStorage(usdc, this.signers.alice.address, "40000"); - //set vault config - const _vaultConfiguration = BigNumber.from( + //Get Vault deployment and set configuration + vault = await getVaultDeploymentAndConfigure( + "opUSDC-Save", "908337486804231642580332837833655270430560746049134246454386846501909299200", - ); - await vault.connect(this.signers.governance).setVaultConfiguration(_vaultConfiguration); - await vault.connect(this.signers.financeOperator).setValueControlParams( - "10000000000", // 10,000 USDC - "1000000000", // 1000 USDC - "1000000000000", // 1,000,000 USDC + "10000000000", + "1000000000", + "1000000000000", ); - const _accountsRoot = getAccountsMerkleRoot([this.signers.alice.address, vaultGateway.address]); - await vault.connect(this.signers.governance).setWhitelistedAccountsRoot(_accountsRoot); - - let accountsProof = getAccountsMerkleProof( - [this.signers.alice.address, vaultGateway.address], - vaultGateway.address, - ); - - depositAmountUSDC = BigNumber.from("1000").mul(to_10powNumber_BN("6")); + //Get accounts proof + const _accountsRoot = getAccountsMerkleRoot([alice.address, vaultGateway.address]); + await vault.connect(owner).setWhitelistedAccountsRoot(_accountsRoot); + let accountsProof = getAccountsMerkleProof([alice.address, vaultGateway.address], vaultGateway.address); + //Sign VaultGateway permit to use Alice's USDC const deadline = ethers.constants.MaxUint256; - const sig = await getPermitSignature(this.signers.alice, usdc, vaultGateway.address, depositAmountUSDC, deadline, { + const sig = await getPermitSignature(alice, usdc, vaultGateway.address, depositAmountUSDC, deadline, { version: "2", nonce: BigNumber.from("1"), }); - - let dataMetaVaultPermit = ethers.utils.defaultAbiCoder.encode( + let permitVaultGateway = ethers.utils.defaultAbiCoder.encode( ["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"], - [this.signers.alice.address, vaultGateway.address, depositAmountUSDC, deadline, sig.v, sig.r, sig.s], + [alice.address, vaultGateway.address, depositAmountUSDC, deadline, sig.v, sig.r, sig.s], ); + //Vault Gateway deposit() Call const dataCall = vaultGateway.interface.encodeFunctionData("deposit", [ vault.address, depositAmountUSDC, BigNumber.from("0"), - dataMetaVaultPermit, + permitVaultGateway, accountsProof, ]); + //RelayRequest Call const provider = ethers.getDefaultProvider(); const feeData = await provider.getFeeData(); const gasPrice = feeData.gasPrice ? feeData.gasPrice.toString() : ""; relayRequest = { request: { - from: this.signers.alice.address, + from: alice.address, to: vaultGateway.address, value: "0", gas: "1000000", - nonce: (await forwarder.getNonce(this.signers.alice.address)).toString(), + nonce: (await forwarder.getNonce(alice.address)).toString(), data: dataCall, validUntilTime: ethers.constants.MaxUint256.toString(), }, @@ -194,7 +188,7 @@ describe("TokenPaymaster", function () { maxFeePerGas: gasPrice, maxPriorityFeePerGas: gasPrice, transactionCalldataGasUsed: "0", - relayWorker: maker.address, + relayWorker: relayer.address, paymaster: paymaster.address, forwarder: forwarder.address, paymasterData, @@ -202,205 +196,112 @@ describe("TokenPaymaster", function () { }, }; - const chainId = +(await getChainId()); //defaultEnvironment.chainId; - const dataToSign = new TypedRequestData( - defaultGsnConfig.domainSeparatorName, - chainId, - forwarder.address, - relayRequest, - ); + //Sign USDC permit + const sig2 = await getPermitSignature(alice, usdc, paymaster.address, depositAmountUSDC, deadline, { + version: "2", + }); - metaTxSignature = await signTypedData(this.signers.alice.provider, this.signers.alice.address, dataToSign); + permitData = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"], + [alice.address, paymaster.address, depositAmountUSDC, deadline, sig2.v, sig2.r, sig2.s], + ); }); after(async function () { await GsnTestEnvironment.stopGsn(); }); - // describe("#preRelayedCall", function () { - // let testHub: TestHub - // describe("reverts if", function () { - // before(async function () { - // testHub = await deployTestHub(false, owner) as TestHub - // await paymaster.setRelayHub(testHub.address) - // }) - - // it("should reject if not enough balance", async () => { - // const req = mergeRelayRequest(relayRequest, { paymasterData: paymasterData }); - // console.log(req); - // await expect(testHub.callPreRC(req, metaTxSignature, '0x', 1e6)).to.revertedWith("ERC20: insufficient allowance"); - // }) - - // it("should reject if unknown paymasterData", async () => { - // const req = mergeRelayRequest(relayRequest, { paymasterData: '0x1234' }) - // const signature = await getEip712Signature(web3, new TypedRequestData(defaultGsnConfig.domainSeparatorName, 1, forwarder.address, req)) - // assert.match(await revertReason(testHub.callPreRC(req, signature, '0x', 1e6)), /paymasterData: invalid length for Uniswap v1 exchange address/) - // }) - - // it("should reject if unsupported uniswap in paymasterData", async () => { - // const req = mergeRelayRequest(relayRequest, { paymasterData: web3.eth.abi.encodeParameter('address', nonUniswap) }) - // const signature = await getEip712Signature(web3, new TypedRequestData(defaultGsnConfig.domainSeparatorName, 1, forwarder.address, req)) - // assert.match(await revertReason(testHub.callPreRC(req, signature, '0x', 1e6)), /unsupported token uniswap/) - // }) - // }) - - // context('with funded recipient', function () { - // before(async function () { - // await token.mint(5e18.toString()) - // await token.transfer(recipient.address, 5e18.toString()) - // }) - - // it('should reject if no token approval', async () => { - // const req = mergeRelayRequest(relayRequest, { paymasterData: web3.eth.abi.encodeParameter('address', uniswap.address) }) - // assert.match(await revertReason(testHub.callPreRC(req, signature, '0x', 1e6)), transferErc20Error) - // }) - - // context('with token approved for paymaster', function () { - // before(async function () { - // await recipient.execute(token.address, token.contract.methods.approve(paymaster.address, MAX_INTEGER.toString()).encodeABI()) - // }) - - // // deliberately removing this functionality as a bit redundant - just pass the token at all times - // it.skip('callPreRC should succeed and return default token/uniswap', async () => { - // const ret: any = await testHub.callPreRC.call(relayRequest, signature, '0x', 1e6) - // const decoded = web3.eth.abi.decodeParameters(['address', 'address', 'address', 'address'], ret.context) - // assert.equal(decoded[2], token.address) - // assert.equal(decoded[3], uniswap.address) - // }) - - // it('callPreRC should succeed with specific token/uniswap', async () => { - // const req = mergeRelayRequest(relayRequest, { paymasterData: web3.eth.abi.encodeParameter('address', uniswap.address) }) - // const signature = await getEip712Signature(web3, new TypedRequestData(defaultGsnConfig.domainSeparatorName, 1, forwarder.address, req)) - // const ret: any = await testHub.callPreRC.call(req, signature, '0x', 1e6) - // const decoded = web3.eth.abi.decodeParameters(['address', 'address', 'address', 'address'], ret.context) as any - // assert.equal(decoded[2], token.address) - // assert.equal(decoded[3], uniswap.address) - // }) - // }) - // }) - // }) - describe("#relayedCall()", function () { const paymasterDeposit = (1e18).toString(); before(async () => { - // TODO: not needed. use startGsn instead - // await GsnTestEnvironment.startGsn("mainnet"); await setTokenBalanceInStorage(usdc, owner.address, "40000"); const stake = parseUnits("10000", 6).toString(); await usdc.connect(owner).approve(stakeManager.address, stake); - await registerAsRelayServer(usdc, stakeManager, maker, owner, stake, relayHub); + await registerAsRelayServer(usdc, stakeManager, relayer, owner, stake, relayHub); await relayHub.depositFor(paymaster.address, { value: paymasterDeposit }); await paymaster.setRelayHub(relayHub.address); }); - // it('should reject if incorrect signature', async () => { - // const wrongSignature = await getEip712Signature( - // web3, - // new TypedRequestData( - // defaultGsnConfig.domainSeparatorName, - // 222, - // forwarder.address, - // relayRequest - // ) - // ) - // const gas = 5000000 - - // const req = mergeRelayRequest(relayRequest, { paymasterData: web3.eth.abi.encodeParameter('address', uniswap.address) }) - // const relayCall: any = await hub.relayCall.call(defaultGsnConfig.domainSeparatorName, 1e06, req, wrongSignature, '0x', { - // from: relay, - // gas - // }) - // assert.equal(decodeRevertReason(relayCall.returnValue), 'FWD: signature mismatch') - // }) + it("should reject if incorrect signature", async () => { + const dataToSign = new TypedRequestData( + defaultGsnConfig.domainSeparatorName, + 222, //wrong chainId + forwarder.address, + relayRequest, + ); + + //Sign Meta Transaction + const wrongSignature = await signTypedData(relayer.provider, relayer.address, dataToSign); + + const externalGasLimit = (5e6).toString(); + const relayCall = await relayHub + .connect(relayer) + .callStatic.relayCall( + defaultGsnConfig.domainSeparatorName, + (10e6).toString(), + relayRequest, + wrongSignature, + permitData, + { + gasLimit: externalGasLimit, + }, + ); + + expect(decodeRevertReason(relayCall.returnValue)).eq("FWD: signature mismatch"); + }); it("should pay with token to make a call", async function () { - // const _relayRequest: any = cloneRelayRequest(relayRequest); - // _relayRequest.request.nonce = (await forwarder.getNonce(maker.address)).toString(); - // _relayRequest.relayData.maxFeePerGas = (1e9).toString(); - // _relayRequest.relayData.maxPriorityFeePerGas = (1e9).toString(); - // _relayRequest.request.value = BigNumber.from("0").toString(); - // _relayRequest.request.gas = BigNumber.from("1000000").toString(); - - // _relayRequest.relayData.paymasterData = web3.eth.abi.encodeParameter('address', uniswap.address) - - // note that by default, ganache is buggy: getChainId returns 1337 but on-chain "chainid" returns 1. - // only if we pass it "--chainId 1337" the above 2 return the same value... - const chainId = await getChainId(); - - // const dataToSign = new TypedRequestData( - // defaultGsnConfig.domainSeparatorName, - // chainId, - // forwarder.address, - // _relayRequest - // ) - // const signature = await getEip712Signature( - // web3, - // dataToSign - // ) - const preBalance = await relayHub.balanceOf(paymaster.address); - const deadline = ethers.constants.MaxUint256; - const sig = await getPermitSignature(this.signers.alice, usdc, paymaster.address, depositAmountUSDC, deadline, { - version: "2", - }); - - let permitData = ethers.utils.defaultAbiCoder.encode( - ["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"], - [this.signers.alice.address, paymaster.address, depositAmountUSDC, deadline, sig.v, sig.r, sig.s], + const chainId = +(await getChainId()); + const dataToSign = new TypedRequestData( + defaultGsnConfig.domainSeparatorName, + chainId, + forwarder.address, + relayRequest, ); - // await usdc.connect(this.signers.alice).approve(paymaster.address, ethers.constants.MaxUint256); - // console.log(defaultGsnConfig.domainSeparatorName); - // console.log(10e6.toString()); - // console.log(await _relayRequest); - // console.log(metaTxSignature); - console.log("maker pre balance", await maker.getBalance()); - console.log("alice pre balance", await vault.balanceOf(this.signers.alice.address)); + //Sign Meta Transaction + metaTxSignature = await signTypedData(alice.provider, alice.address, dataToSign); + + const preRelayAliceVaultBalance = await vault.balanceOf(alice.address); + const preRelayAliceETHBalance = await alice.getBalance(); + const relayerPreBalance = await relayer.getBalance(); + //Execute Relay Call const externalGasLimit = (5e6).toString(); - const relayCall = await relayHub - .connect(maker) - .relayCall(defaultGsnConfig.domainSeparatorName, (10e6).toString(), relayRequest, metaTxSignature, permitData, { - gasLimit: externalGasLimit, - }); - - const ret = await relayCall.wait(); - console.log("maker post balance", await maker.getBalance()); - console.log("alice post balance", await vault.balanceOf(this.signers.alice.address)); - - const rejected = ret.events?.find(log => log.event === "TransactionRejectedByPaymaster"); - // @ts-ignore - if (rejected != null) { - `Rejected with reason: ${decodeRevertReason(rejected?.args?.reason) as string}`; - } - // const relayed = ret.events?.find(log => log.event === "TransactionRelayed"); - // @ts-ignore - // const events = await paymaster.getPastEvents(); - // const chargedEvent = events.find((e: any) => e.event === "TokensCharged"); - - // console.log({ relayed, chargedEvent }) - // @ts-ignore - // console.log("charged: ", relayed?.args.charge.toString()); - // @ts-ignore - // assert.equal(relayed!.args.status, 0) - // const postTokens = await usdc.balanceOf(this.signers.alice.address); - // const usedTokens = preTokens.sub(postTokens); - - // console.log("alice tokens balance change (used ETH): ", usedTokens.toString()); - // // @ts-ignore - // console.log("reported charged tokens in TokensCharged: ", chargedEvent.args.tokenActualCharge.toString()); - // @ts-ignore - // const expectedTokenCharge = await uniswap.getTokenToEthOutputPrice(chargedEvent.args.ethActualCharge) - // assert.closeTo(usedTokens.toNumber(), expectedTokenCharge.toNumber(), 1000) - // const postBalance = await hub.balanceOf(paymaster.address) - - // assert.ok(postBalance >= preBalance, - // `expected paymaster balance not to be reduced: pre=${preBalance.toString()} post=${postBalance.toString()}`) - // // TODO: add test for relayed.args.charge, once gasUsedWithoutPost parameter is fixed (currently, its too high, and Paymaster "charges" too much) - // const postPaymasterTokens = await token.balanceOf(paymaster.address) - // console.log('Paymaster "earned" tokens:', postPaymasterTokens.sub(prePaymasterTokens).toString()) - // console.log('Paymaster "earned" deposit on RelayHub:', postBalance.sub(preBalance).toString()) + expect( + await relayHub + .connect(relayer) + .relayCall( + defaultGsnConfig.domainSeparatorName, + (10e6).toString(), + relayRequest, + metaTxSignature, + permitData, + { + gasLimit: externalGasLimit, + }, + ), + ) + .to.emit(relayHub, "TransactionRelayed") + .to.emit(paymaster, "TokensCharged"); + + const postRelayAliceVaultBalance = await vault.balanceOf(alice.address); + const postRelayAliceETHBalance = await alice.getBalance(); + + //Alice Vault tokens increased + expect(postRelayAliceVaultBalance).gt(preRelayAliceVaultBalance); + + //Alice ETH balance don't change + expect(postRelayAliceETHBalance).eq(preRelayAliceETHBalance); + + //Relayer receiced more ETH than spent + const ethReceived = await relayHub.balanceOf(relayer.address); + expect(ethReceived).gt(0); + await relayHub.connect(relayer).withdraw(relayer.address, ethReceived); + const relayerPostBalance = await relayer.getBalance(); + expect(relayerPostBalance).gt(relayerPreBalance); }); }); }); diff --git a/test/test-opty/utils.ts b/test/test-opty/utils.ts index 9225c70a8..534076f64 100644 --- a/test/test-opty/utils.ts +++ b/test/test-opty/utils.ts @@ -2,7 +2,7 @@ // Please do not keep this file under helpers/utils as it imports hre from hardhat import { BigNumber, BigNumberish, Contract, Signature, Signer } from "ethers"; import { getAddress, parseEther, splitSignature } from "ethers/lib/utils"; -import hre, { ethers } from "hardhat"; +import hre, { deployments, ethers } from "hardhat"; import ethTokens from "@optyfi/defi-legos/ethereum/tokens/wrapped_tokens"; import polygonTokens from "@optyfi/defi-legos/polygon/tokens"; import avaxTokens from "@optyfi/defi-legos/avalanche/tokens"; @@ -16,9 +16,11 @@ import { ERC20Permit, RelayHub__factory, RelayRegistrar__factory, + Vault, + Vault__factory, } from "../../typechain"; import { fundWalletToken, getBlockTimestamp } from "../../helpers/contracts-actions"; -import { StakeManager, TokenGasCalculator, RelayHub, TestToken, RelayRegistrar } from "../../typechain"; +import { StakeManager, TokenGasCalculator, RelayHub, RelayRegistrar } from "../../typechain"; import { deployContract } from "../../helpers/helpers"; import { PrefixedHexString } from "ethereumjs-util"; import { GasUsedEvent } from "../../typechain/TokenGasCalculator"; @@ -32,9 +34,7 @@ import { RelayRequest, defaultEnvironment, splitRelayUrlForRegistrar, - TypedRequestData, } from "@opengsn/common"; -import { defaultGsnConfig } from "@opengsn/provider"; const setStorageAt = (address: string, slot: string, val: string): Promise => hre.network.provider.send("hardhat_setStorageAt", [address, slot, val]); @@ -550,3 +550,24 @@ export async function deployHub( hub._secretRegistrarInstance = relayRegistrar; return hub; } + +export async function getVaultDeploymentAndConfigure( + vaultName: string, + vaultConfiguration: string, + maxUserdepoist: string, + minUserDeposit: string, + maxVaultTVL: string, +): Promise { + const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get(vaultName)).address; + let vault = await ethers.getContractAt(Vault__factory.abi, OPUSDCGROW_VAULT_ADDRESS); + //set vault config + const _vaultConfiguration = BigNumber.from(vaultConfiguration); + await vault.setVaultConfiguration(_vaultConfiguration); + await vault.setValueControlParams( + maxUserdepoist, // 10,000 USDC + minUserDeposit, // 1000 USDC + maxVaultTVL, // 1,000,000 USDC + ); + + return vault; +}