Gas as a Service (GaaS)

This example walks though how to allow users to pay for Ethereum transactions with any currency.

Introduction

When building smart contracts that interact with RenVM, you can use patterns to enable methods to be called via the OpenZeppelin Gas Station Network (GSN). The GSN is a decentralized solution for solving user onboarding to Ethereum applications. It allows dapps to pay for their users' transactions in a secure way, so users don’t need to hold ETH to pay for their gas or even set up an account.

This can be particularly useful to handle new users who don’t have ETH in their wallet yet. Not only will this help new users get started when shifting in non-Ethereum assets, but it can also allow users to never need to hold ETH, and in turn, pay for on-chain actions using any asset that they already have available.

To play around with a live version of this example, check out our interoperabilty example app.

Building a GSN dapp

In this tutorial we’ll create an adapter contract that will shift Bitcoin on to Ethereum, swap that Bitcoin for Ethereum, and then send ETH to a user's brand new, empty wallet.

Users will submit signed transactions to the contract via the GSN, and in turn nodes on the GSN will call your contract through the GSN RelayHub contract. In return for paying gas to submit that transaction, the GSN node will receive a refund in ETH from your adapter contract. Although in this tutorial you'll cover these gas costs in full for your users, there are a number of different payment strategies you can implement to cover the gas costs you incur.

The Adapter

We’ll first have our contract extend OpenZeppelin’s GSNRecipient.

pragma solidity ^0.5.8;
import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/GSN/GSNRecipient.sol";
contract UniswapExchangeAdapter is GSNRecipient {
...
}

Next we’ll implement acceptRelayedCall, _preRelayedCall, and _postRelayedCall which are required for GSNRecipient. Given that all of our methods will be paid for by the user, let’s accept all calls made and leave the other methods empty.

function acceptRelayedCall(
address,
address,
bytes calldata,
uint256,
uint256,
uint256,
uint256,
bytes calldata,
uint256
) external view returns (uint256, bytes memory) {
return _approveRelayedCall();
}
function _preRelayedCall(bytes memory context) internal returns (bytes32) {
}
function _postRelayedCall(bytes memory context, bool, uint256 actualCharge, bytes32) internal {
}

Finally we’ll create a shiftInWithSwap method that moves Bitcoin on to Ethereum, swaps it for ETH, and then sends that ETH to the user's address.

function shiftInWithSwap(
address payable _to,
uint256 _amount,
bytes32 _nHash,
bytes calldata _sig
) external returns (uint256 ethBought){
// Shift in
bytes32 pHash = keccak256(abi.encode(_to));
uint256 shiftedInAmount = registry.getShifterBySymbol("zBTC").shiftIn(pHash, _amount, _nHash, _sig);
// Approve and trade the shifted tokens with the uniswap exchange.
require(IERC20(token).approve(address(exchange), shiftedInAmount));
ethBought = exchange.tokenToEthSwapInput(shiftedInAmount, uint256(1), uint256(block.timestamp * 2));
// Send proceeds to the User
_to.transfer(ethBought);
}

The Front-end

Interacting with the GSN client side is made simple with OpenZeppelin’s network.js library.

import {
fromConnection,
ephemeral
} from "@openzeppelin/network/lib";
const REACT_APP_TX_FEE = 100;
const signKey = ephemeral();
const gasPrice = 2000000000;
const relay_client_config = {
txfee: REACT_APP_TX_FEE,
force_gasPrice: gasPrice, //override requested gas price
gasPrice: gasPrice, //override requested gas price
force_gasLimit: 500000, //override requested gas limit.
gasLimit: 500000, //override requested gas limit.
verbose: true
};
...
const web3Context = await fromConnection(
"https://kovan.infura.io/v3/7be66f167c2e4a05981e2ffc4653dec2",
{
gsn: { signKey, ...relay_client_config }
}
)

Improving Functionality - The Ultimate Way to Stack Sats

Integrating with the GSN lowers the barriers for Bitcoin holders to interact with the growing DeFi ecosystem on Ethereum, allowing them to access exchange, lending, and other smart contract platforms using the assets they already own. Beyond just shifting in, developers can implement a whole host of interesting services for Bitcoin users once they’ve shifted it into their Ethereum wallet. Let’s upgrade our adapter to enable users to stack sats (through lending interest) while they stack sats (accumulate and hold BTC).

To start, let’s create a new shiftInWithGasBalance method, which will keep a small amount of shifted bitcoin in the contract for the user to pay gas transactions from.

function shiftInWithGasBalance(
address _sender,
uint256 _amount,
bytes32 _nHash,
bytes calldata _sig
) external {
// Calculate fee from an on-chain oracle (uniswap)
uint256 gasCostInBtc = getBtcEthCost();
// Shift in
bytes32 pHash = keccak256(abi.encode(_sender));
uint256 shiftedInAmount = registry.getShifterBySymbol("zBTC").shiftIn(pHash, _amount, _nHash, _sig);
uint256 gasAllocation = (shiftedInAmount / 10) - gasCostInBtc;
increaseGasBalance(_sender, gasAllocation);
// Send the user their funds minus their remaining gas balance
registry.getTokenBySymbol("zBTC").transfer(_sender, shiftedInAmount-gasAllocation);
}

Next we’ll implement startStackingSats and stopStackingSats methods, which will move Shifted Bitcoin in and out of an algorithmic money market similar to one implemented by bZx. Ownership of assets would always remain with the user’s Ethereum wallet, and the adapter would deduct a BTC fee from the user’s small gas balance held in the contract.

function startStackingSats (uint256 _amount) public {
// Calculate fee from an on-chain oracle (uniswap)
uint256 gasCostInBtc = getBtcEthCost();
address _sender = _msgSender();
require(btcGasBalances[_sender] > gasCostInBtc);
// Deposit to lending pool
startLending(_msgSender(), _amount);
}
function stopStackingSats (uint256 _amount) public {
// Calculate fee from an on-chain oracle (uniswap)
uint256 gasCostInBtc = getBtcEthCost();
address _sender = _msgSender();
require(btcGasBalances[_sender] > gasCostInBtc);
decreaseGasBalance(_sender, gasCostInBtc);
// Withdraw from lending pool
stopLending(_msgSender(), _amount);
}

Finally, let’s allow users to top off their gas balance or withdraw from it whenever they’d like.

function addGas (uint256 _amount) public {
// Calculate fee from an on-chain oracle (uniswap)
uint256 gasCostInBtc = getBtcEthCost();
address _sender = _msgSender();
require(btcGasBalances[_sender] > gasCostInBtc);
zbtcToken.transferFrom(_sender, address(this), _amount);
increaseGasBalance(_sender, _amount-gasCostInBtc);
}
function removeGas (uint256 _amount) public {
// Calculate fee from an on-chain oracle (uniswap)
uint256 gasCostInBtc = getBtcEthCost();
address _sender = _msgSender();
require(btcGasBalances[_sender] > gasCostInBtc && btcGasBalances[_sender] >= _amount);
zbtcToken.transfer(_sender, _amount-gasCostInBtc);
decreaseGasBalance(_sender, _amount);
}

That’s it! Users can now doubly stack sats and remain in complete control of their assets at all times. Better yet, they don’t need to interact with any asset other than Bitcoin, allowing developers to continue to abstract Ethereum specific concepts on the front-end.

Check out the entire project repo here.