From 4bb6d4f36a3e9238bd1895e2fbf4787094fb8f24 Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Sat, 16 Apr 2022 14:27:24 -0700 Subject: [PATCH 01/14] Create OpenGSNSpec.md --- OpenGSNSpec.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 OpenGSNSpec.md diff --git a/OpenGSNSpec.md b/OpenGSNSpec.md new file mode 100644 index 0000000..a43f3a3 --- /dev/null +++ b/OpenGSNSpec.md @@ -0,0 +1,9 @@ +According to the [OpenGSN docs](https://docs.opengsn.org/contracts/#paying-for-your-user-s-meta-transaction), the main logic to implement is the Paymaster. We want to create a paymaster that handles the deposits for all contracts, but defers to the individual contracts when it comes to defining the conditions for gas reimbursement, and depositing the gas funds in advance. + + +# OpenContract + + + +# OpenContractsPaymaster + From e154e7ac60db208c1abc9b54d069480c49a1076c Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 18:54:36 -0700 Subject: [PATCH 02/14] Update OpenGSNSpec.md --- OpenGSNSpec.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/OpenGSNSpec.md b/OpenGSNSpec.md index a43f3a3..0991d37 100644 --- a/OpenGSNSpec.md +++ b/OpenGSNSpec.md @@ -1,9 +1,48 @@ -According to the [OpenGSN docs](https://docs.opengsn.org/contracts/#paying-for-your-user-s-meta-transaction), the main logic to implement is the Paymaster. We want to create a paymaster that handles the deposits for all contracts, but defers to the individual contracts when it comes to defining the conditions for gas reimbursement, and depositing the gas funds in advance. +The [OpenGSN docs](https://docs.opengsn.org/contracts/#paying-for-your-user-s-meta-transaction) clarify that the main logic to implement is the Paymaster. To achieve a design that is as simple as possible, we will only attempt to allow gasless transactions via enclaves. # OpenContract +Inside an Open Contract (let's assume FiatSwap for now), the `OpenContract` parent class will expose a minimal API to deposit into the `OpenContractsPaymaster` and define the conditions under which the deposit can be used up. This will likely involve just a single function call. +E.g. inside the `offerTokens` function of FiatSwap, one could call +``` +prepayGas(selector=this.buyTokens.selector, gasID=offerID, ...gasParams) +``` +which: + - calls `OpenContractsPaymaster.prepayGas(msg.sender, selector, gasID, ...gasParams)`, where `gasParams` are (which?) parameters OpenGSN requires us to set. + - in doing so, transfers enough ETH to the paymaster to pay for gas, and enough OPN to pay the Hub. + +FiatSwap would prepay for individual offers, hence set `gasID=offerID`. + +Inside `oracle.py`, we add an optional `gasID` arg to the submit-function: + +``` +session.submit(..., function="buyTokens", gasID=offerID) +``` + +This will inform the frontend that the OpenGSN ethereum provider should be used. On-chain, the paymaster is supposed to ensure that this call will only be reimbursed if enough ETH and OPN were deposited for this specific gasID of this specific function from a user of this specific contract. # OpenContractsPaymaster +The Paymaster implements the actual prepayment check, via: + +``` +prepayGas(depositor, selector, gasID, ...gasParams) +``` + +which: + - updates `ethBalance[msg.sender][gasID] += msg.value` + - grabs the OPN via `OPNToken.transfer(tx.origin, address(this), opnAmount)`, which requires that the frontend asked the depositor to approve their OPN for the paymaster *!!!! EDIT: ppl say tx.origin is insecure. understand more deeply. https://medium.com/coinmonks/solidity-tx-origin-attacks-58211ad95514 * + - updates `opnBalance[msg.sender][gasID] =+ opnAmount` + + +Later, OpenGSN will call + +``` +preRelayedCall(request, approvalData, maxGas, ...) +``` + +which needs to: +- check that enough OPN and ETH were deposited for a given gasID for the given contract function *!! problem: can only trust gasID after the OpenContractsVerifier. Might need to merge paymaster and verifier?* +- then forwardthe call to the verifier From 3699f554cf5bd6a39e172f287b9d75bafdcf16ae Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 20:00:17 -0700 Subject: [PATCH 03/14] Update OpenGSNSpec.md --- OpenGSNSpec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenGSNSpec.md b/OpenGSNSpec.md index 0991d37..9d5fc70 100644 --- a/OpenGSNSpec.md +++ b/OpenGSNSpec.md @@ -32,9 +32,9 @@ prepayGas(depositor, selector, gasID, ...gasParams) ``` which: - - updates `ethBalance[msg.sender][gasID] += msg.value` + - updates `ethBalance[msg.sender][selector][gasID] += msg.value` - grabs the OPN via `OPNToken.transfer(tx.origin, address(this), opnAmount)`, which requires that the frontend asked the depositor to approve their OPN for the paymaster *!!!! EDIT: ppl say tx.origin is insecure. understand more deeply. https://medium.com/coinmonks/solidity-tx-origin-attacks-58211ad95514 * - - updates `opnBalance[msg.sender][gasID] =+ opnAmount` + - updates `opnBalance[msg.sender][selector][gasID] += opnAmount` Later, OpenGSN will call From e81dc30729b03e0ad4222bffa7e5fe4f5b6ba96e Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 20:00:54 -0700 Subject: [PATCH 04/14] Update OpenGSNSpec.md --- OpenGSNSpec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenGSNSpec.md b/OpenGSNSpec.md index 9d5fc70..fe47a96 100644 --- a/OpenGSNSpec.md +++ b/OpenGSNSpec.md @@ -7,7 +7,7 @@ Inside an Open Contract (let's assume FiatSwap for now), the `OpenContract` pare E.g. inside the `offerTokens` function of FiatSwap, one could call ``` -prepayGas(selector=this.buyTokens.selector, gasID=offerID, ...gasParams) +prepayGas{value: msg.value}(selector=this.buyTokens.selector, gasID=offerID, ...gasParams) ``` which: - calls `OpenContractsPaymaster.prepayGas(msg.sender, selector, gasID, ...gasParams)`, where `gasParams` are (which?) parameters OpenGSN requires us to set. From af51d432b170309e036ce8ad24fb2e805480a937 Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 20:34:35 -0700 Subject: [PATCH 05/14] Update OpenGSNSpec.md --- OpenGSNSpec.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/OpenGSNSpec.md b/OpenGSNSpec.md index fe47a96..b987bb4 100644 --- a/OpenGSNSpec.md +++ b/OpenGSNSpec.md @@ -7,10 +7,10 @@ Inside an Open Contract (let's assume FiatSwap for now), the `OpenContract` pare E.g. inside the `offerTokens` function of FiatSwap, one could call ``` -prepayGas{value: msg.value}(selector=this.buyTokens.selector, gasID=offerID, ...gasParams) +prepayGas(selector=this.buyTokens.selector, gasID=offerID, ...gasParams) ``` which: - - calls `OpenContractsPaymaster.prepayGas(msg.sender, selector, gasID, ...gasParams)`, where `gasParams` are (which?) parameters OpenGSN requires us to set. + - calls `OpenContractsPaymaster.prepayGas{value: msg.value}(msg.sender, selector, gasID, ...gasParams)`, where `gasParams` are (which?) parameters OpenGSN requires us to set. - in doing so, transfers enough ETH to the paymaster to pay for gas, and enough OPN to pay the Hub. FiatSwap would prepay for individual offers, hence set `gasID=offerID`. @@ -44,5 +44,6 @@ preRelayedCall(request, approvalData, maxGas, ...) ``` which needs to: -- check that enough OPN and ETH were deposited for a given gasID for the given contract function *!! problem: can only trust gasID after the OpenContractsVerifier. Might need to merge paymaster and verifier?* -- then forwardthe call to the verifier +- check that enough OPN and ETH were deposited for a given gasID for the given contract function +- return flag "rejectOnRecipientRevert" +- then forward the call to the verifier, in a way that tells it to revert if the gasID wasn't signed From 9e645e7cc5be88d0792f0c0cb9c3a9d9506c0423 Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 20:38:31 -0700 Subject: [PATCH 06/14] Update OpenGSNSpec.md --- OpenGSNSpec.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/OpenGSNSpec.md b/OpenGSNSpec.md index b987bb4..18f440a 100644 --- a/OpenGSNSpec.md +++ b/OpenGSNSpec.md @@ -23,6 +23,8 @@ session.submit(..., function="buyTokens", gasID=offerID) This will inform the frontend that the OpenGSN ethereum provider should be used. On-chain, the paymaster is supposed to ensure that this call will only be reimbursed if enough ETH and OPN were deposited for this specific gasID of this specific function from a user of this specific contract. +Set default gasID to 0, and make sure it is included in `oracleSignature` alongside nonce. + # OpenContractsPaymaster The Paymaster implements the actual prepayment check, via: @@ -47,3 +49,8 @@ which needs to: - check that enough OPN and ETH were deposited for a given gasID for the given contract function - return flag "rejectOnRecipientRevert" - then forward the call to the verifier, in a way that tells it to revert if the gasID wasn't signed + + +# Updates to Verifier + +Literaly just make `oracleMsgHash` depend on gasID From e28fba4199f7f1858967f0bf9e32a034a50aa863 Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 20:39:16 -0700 Subject: [PATCH 07/14] Update OpenGSNSpec.md --- OpenGSNSpec.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/OpenGSNSpec.md b/OpenGSNSpec.md index 18f440a..2d1f401 100644 --- a/OpenGSNSpec.md +++ b/OpenGSNSpec.md @@ -54,3 +54,7 @@ which needs to: # Updates to Verifier Literaly just make `oracleMsgHash` depend on gasID + +# Updates to Frontend + +If gasID is nonzero, switch to OpenGSN ethereum provider From 43c24229e2500877eb68e4c2638c0549f53cfb9a Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 20:39:48 -0700 Subject: [PATCH 08/14] Update OpenGSNSpec.md --- OpenGSNSpec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenGSNSpec.md b/OpenGSNSpec.md index 2d1f401..5562956 100644 --- a/OpenGSNSpec.md +++ b/OpenGSNSpec.md @@ -39,7 +39,7 @@ which: - updates `opnBalance[msg.sender][selector][gasID] += opnAmount` -Later, OpenGSN will call +Later, OpenGSN will call: ``` preRelayedCall(request, approvalData, maxGas, ...) @@ -47,7 +47,7 @@ preRelayedCall(request, approvalData, maxGas, ...) which needs to: - check that enough OPN and ETH were deposited for a given gasID for the given contract function -- return flag "rejectOnRecipientRevert" +- return flag "rejectOnRecipientRevert" to allow Verifier to reject invalid gasID - then forward the call to the verifier, in a way that tells it to revert if the gasID wasn't signed From f5d95f9747f1def20d4a50d831998565133bdbbb Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 20:57:34 -0700 Subject: [PATCH 09/14] Create OpenContractGSN.sol --- solidity_contracts/OpenContractGSN.sol | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 solidity_contracts/OpenContractGSN.sol diff --git a/solidity_contracts/OpenContractGSN.sol b/solidity_contracts/OpenContractGSN.sol new file mode 100644 index 0000000..0a24e26 --- /dev/null +++ b/solidity_contracts/OpenContractGSN.sol @@ -0,0 +1,29 @@ +pragma solidity >=0.8.0; + +contract OpenContract { + OpenContractsHub private hub = OpenContractsHub(0x059dE2588d076B67901b07A81239286076eC7b89); + OpenContractsPaymaster private paymaster = OpenContractsPaymaster(0x059dE2588d076B67901b07A81239286076eC7b89); + + // this call tells the Hub which oracleID is allowed for a given contract function + function setOracleHash(bytes4 selector, bytes32 oracleHash) internal { + hub.setOracleHash(selector, oracleHash); + } + + function prepayGas(...) internal { + paymaster.prepayGas(...); + } + + modifier requiresOracle { + // the Hub uses the Verifier to ensure that the calldata came from the right oracleID + require(msg.sender == address(hub), "Can only be called via Open Contracts Hub."); + _; + } +} + +interface OpenContractsHub { + function setOracleHash(bytes4, bytes32) external; +} + +interface OpenContractsPaymaster { + function prepayGas(...) external; +} From 1618b2d6ecc299127e952458bf52aa10a9e65016 Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 20:58:29 -0700 Subject: [PATCH 10/14] Update OpenContractGSN.sol --- solidity_contracts/OpenContractGSN.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/solidity_contracts/OpenContractGSN.sol b/solidity_contracts/OpenContractGSN.sol index 0a24e26..14729a5 100644 --- a/solidity_contracts/OpenContractGSN.sol +++ b/solidity_contracts/OpenContractGSN.sol @@ -9,8 +9,8 @@ contract OpenContract { hub.setOracleHash(selector, oracleHash); } - function prepayGas(...) internal { - paymaster.prepayGas(...); + function prepayGas(bytes4 selector, bytes32 gasID) internal { + paymaster.prepayGas(selector, gasID); } modifier requiresOracle { @@ -25,5 +25,5 @@ interface OpenContractsHub { } interface OpenContractsPaymaster { - function prepayGas(...) external; + function prepayGas(bytes3, bytes32) external; } From 56b456fe0d2194216c98045582a1b85cffa63c63 Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 20:58:49 -0700 Subject: [PATCH 11/14] Update OpenContractGSN.sol --- solidity_contracts/OpenContractGSN.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity_contracts/OpenContractGSN.sol b/solidity_contracts/OpenContractGSN.sol index 14729a5..f72313b 100644 --- a/solidity_contracts/OpenContractGSN.sol +++ b/solidity_contracts/OpenContractGSN.sol @@ -9,7 +9,7 @@ contract OpenContract { hub.setOracleHash(selector, oracleHash); } - function prepayGas(bytes4 selector, bytes32 gasID) internal { + function prepayGas(bytes4 selector, bytes32 gasID) internal { // any additional gas params? paymaster.prepayGas(selector, gasID); } From 8e22b228baa989112c3107c0af25bbb1917f993e Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 21:04:49 -0700 Subject: [PATCH 12/14] Create OpenContractsPaymaster.sol --- solidity_contracts/OpenContractsPaymaster.sol | 1 + 1 file changed, 1 insertion(+) create mode 100644 solidity_contracts/OpenContractsPaymaster.sol diff --git a/solidity_contracts/OpenContractsPaymaster.sol b/solidity_contracts/OpenContractsPaymaster.sol new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/solidity_contracts/OpenContractsPaymaster.sol @@ -0,0 +1 @@ + From 68c6231eafb2618d34d4a5d760e452fd5b947b3e Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 21:05:40 -0700 Subject: [PATCH 13/14] Update OpenContractGSN.sol --- solidity_contracts/OpenContractGSN.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/solidity_contracts/OpenContractGSN.sol b/solidity_contracts/OpenContractGSN.sol index f72313b..da4f236 100644 --- a/solidity_contracts/OpenContractGSN.sol +++ b/solidity_contracts/OpenContractGSN.sol @@ -9,7 +9,10 @@ contract OpenContract { hub.setOracleHash(selector, oracleHash); } - function prepayGas(bytes4 selector, bytes32 gasID) internal { // any additional gas params? + function prepayGas(bytes4 selector, bytes32 gasID) internal { + // any additional gas params that need to be defined here? + // goal: minimize params exposed via API subject to everything just working safely + // might need to though, to de paymaster.prepayGas(selector, gasID); } From 95d4f77aaeec8456667430e22ed5411f549fd25f Mon Sep 17 00:00:00 2001 From: Open Contracts <90533597+open-contracts@users.noreply.github.com> Date: Wed, 20 Apr 2022 21:15:32 -0700 Subject: [PATCH 14/14] Update OpenContractsPaymaster.sol --- solidity_contracts/OpenContractsPaymaster.sol | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/solidity_contracts/OpenContractsPaymaster.sol b/solidity_contracts/OpenContractsPaymaster.sol index 8b13789..99d4159 100644 --- a/solidity_contracts/OpenContractsPaymaster.sol +++ b/solidity_contracts/OpenContractsPaymaster.sol @@ -1 +1,155 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "@opengsn/contracts/src/forwarder/IForwarder.sol"; +import "@opengsn/contracts/src/BasePaymaster.sol"; + +import "./interfaces/IUniswap.sol"; + +/** + * A Token-based paymaster. + * - each request is paid for by the caller. + * - acceptRelayedCall - verify the caller can pay for the request in tokens. + * - preRelayedCall - pre-pay the maximum possible price for the tx + * - postRelayedCall - refund the caller for the unused gas + */ +contract TokenPaymaster is BasePaymaster { + + function versionPaymaster() external override virtual view returns (string memory){ + return "2.2.3+opengsn.token.ipaymaster"; + } + + + IUniswap[] public uniswaps; + IERC20[] public tokens; + + mapping (IUniswap=>bool ) private supportedUniswaps; + + uint256 public gasUsedByPost; + + constructor(IUniswap[] memory _uniswaps) { + uniswaps = _uniswaps; + + for (uint256 i = 0; i < _uniswaps.length; i++){ + supportedUniswaps[_uniswaps[i]] = true; + tokens.push(IERC20(_uniswaps[i].tokenAddress())); + tokens[i].approve(address(_uniswaps[i]), type(uint256).max); + } + } + + /** + * 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 of Uniswap) + */ + function setPostGasUsage(uint256 _gasUsedByPost) external onlyOwner { + gasUsedByPost = _gasUsedByPost; + } + + // return the payer of this request. + // for account-based target, this is the target account. + function getPayer(GsnTypes.RelayRequest calldata relayRequest) public virtual view returns (address) { + (this); + return relayRequest.request.to; + } + + event Received(uint256 eth); + receive() external override payable { + emit Received(msg.value); + } + + function _getToken(bytes memory paymasterData) internal view returns (IERC20 token, IUniswap uniswap) { + uniswap = abi.decode(paymasterData, (IUniswap)); + require(supportedUniswaps[uniswap], "unsupported token uniswap"); + token = IERC20(uniswap.tokenAddress()); + } + + function _calculatePreCharge( + IERC20 token, + IUniswap uniswap, + GsnTypes.RelayRequest calldata relayRequest, + uint256 maxPossibleGas) + internal + view + returns (address payer, uint256 tokenPreCharge) { + (token); + payer = this.getPayer(relayRequest); + uint256 ethMaxCharge = relayHub.calculateCharge(maxPossibleGas, relayRequest.relayData); + ethMaxCharge += relayRequest.request.value; + tokenPreCharge = uniswap.getTokenToEthOutputPrice(ethMaxCharge); + } + + function _verifyPaymasterData(GsnTypes.RelayRequest calldata relayRequest) internal virtual override view { + // solhint-disable-next-line reason-string + require(relayRequest.relayData.paymasterData.length == 32, "paymasterData: invalid length for Uniswap v1 exchange address"); + } + + function _preRelayedCall( + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData, + uint256 maxPossibleGas + ) + internal + override + virtual + returns (bytes memory context, bool revertOnRecipientRevert) { + (signature, approvalData); + + (IERC20 token, IUniswap uniswap) = _getToken(relayRequest.relayData.paymasterData); + (address payer, uint256 tokenPrecharge) = _calculatePreCharge(token, uniswap, relayRequest, maxPossibleGas); + token.transferFrom(payer, address(this), tokenPrecharge); + return (abi.encode(payer, tokenPrecharge, token, uniswap), false); + } + + function _postRelayedCall( + bytes calldata context, + bool, + uint256 gasUseWithoutPost, + GsnTypes.RelayData calldata relayData + ) + internal + override + virtual + { + (address payer, uint256 tokenPrecharge, IERC20 token, IUniswap uniswap) = abi.decode(context, (address, uint256, IERC20, IUniswap)); + _postRelayedCallInternal(payer, tokenPrecharge, 0, gasUseWithoutPost, relayData, token, uniswap); + } + + function _postRelayedCallInternal( + address payer, + uint256 tokenPrecharge, + uint256 valueRequested, + uint256 gasUseWithoutPost, + GsnTypes.RelayData calldata relayData, + IERC20 token, + IUniswap uniswap + ) internal { + uint256 ethActualCharge = relayHub.calculateCharge(gasUseWithoutPost + gasUsedByPost, relayData); + uint256 tokenActualCharge = uniswap.getTokenToEthOutputPrice(valueRequested + ethActualCharge); + uint256 tokenRefund = tokenPrecharge - tokenActualCharge; + _refundPayer(payer, token, tokenRefund); + _depositProceedsToHub(ethActualCharge, uniswap); + emit TokensCharged(gasUseWithoutPost, gasUsedByPost, ethActualCharge, tokenActualCharge); + } + + function _refundPayer( + address payer, + IERC20 token, + uint256 tokenRefund + ) private { + require(token.transfer(payer, tokenRefund), "failed refund"); + } + + function _depositProceedsToHub(uint256 ethActualCharge, IUniswap uniswap) private { + //solhint-disable-next-line + uniswap.tokenToEthSwapOutput(ethActualCharge, type(uint256).max, block.timestamp+60*15); + relayHub.depositFor{value:ethActualCharge}(address(this)); + } + + event TokensCharged(uint256 gasUseWithoutPost, uint256 gasJustPost, uint256 ethActualCharge, uint256 tokenActualCharge); +}