Skip to content
Open
60 changes: 60 additions & 0 deletions OpenGSNSpec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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{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`.

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.

Set default gasID to 0, and make sure it is included in `oracleSignature` alongside nonce.

# OpenContractsPaymaster

The Paymaster implements the actual prepayment check, via:

```
prepayGas(depositor, selector, gasID, ...gasParams)
```

which:
- 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][selector][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
- 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


# Updates to Verifier

Literaly just make `oracleMsgHash` depend on gasID

# Updates to Frontend

If gasID is nonzero, switch to OpenGSN ethereum provider
32 changes: 32 additions & 0 deletions solidity_contracts/OpenContractGSN.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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(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);
}

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(bytes3, bytes32) external;
}
155 changes: 155 additions & 0 deletions solidity_contracts/OpenContractsPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +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);
}