Confirmations as a Service (CaaS)

This example walks through how to enable faster cross-chain transactions for users with pre-shifted liquidity held in a smart contract.

Introduction

One thing that developers can do to improve cross-chain user experience is to help users complete actions faster by using funds that have already been shifted. These funds can be accessed in a variety of trustful and trust-less ways, however the goal is the same - facilitate a cross-chain transaction in a shorter time than it would take the user to first fully shift in an asset and then complete an action.

This pattern adds a layer of trust into an application in return for convenience. If developers are in a position to drop the confirmation requirement lower than RenVM’s, users can initiate core actions quicker from their source wallet without the need to fully shift in assets beforehand. Services that could benefit most from this pattern are those that offer semi-centralized but non-custodial services that care about speed, such as DEXs.

In this tutorial we’ll illustrate a simple way to access funds in a trust-full way via funds held in a smart contract, and then show how the contract can be improved to remove the ability for developers to act maliciously.

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

Why do this?

If permission-less and decentralized smart contracts have proven useful for anything, it’s pooling together assets for better liquidity for exchange or lending. By having a centralized service interact with these decentralized contracts, your application can offer users better liquidity and competitive performance while also maintaining a non-custodial architecture.

Flow of Funds

In this tutorial we will take one of the simplest approaches possible, and then point out areas for potential improvement. The process is as follows:

  1. Generate a gateway address for the user that will send funds to your adapter contract

  2. Monitor the source blockchain network for a transaction to the gateway address

  3. Once an adequate number of transactions are reached for your application, call the desired destination function for the user using funds already in the adapter contract

  4. Once an adequate number of transactions are reached for RenVM, retrieve the signature and complete the shift in to bring the contract’s funds back to the original value

To keep things simple we will build a single adapter contract which will:

  • Maintain a float of shifted assets owned by your wallet

  • Automatically add shifted in funds to the contract’s balance

  • Conduct on-chain actions for your users using the funds from the contract’s balance

Users will interact with the service through a good old fashioned REST API, which will:

  • Provide the gateway address to send funds to for their desired trade

  • Monitor the BTC network for confirmations

  • Automatically execute a trade via the adapter contract after a custom number of confirmations

  • Complete the shift in and replenish the float when a larger number of confirmations is completed

The Adapter Contract

To illustrate an extreme example, we will create a Uniswap adapter contract that allows users to execute a BTC/ETH trade from after 0 BTC confirmations. The end result will be a service for swapping in between the two assets within a couple minutes.

To start let’s write the initial structure which establishes ownership, an integration with the GSN, and an integration with Uniswap which gives us access to liquidity that already exists.

contract UniswapExchangeAdapter is GSNRecipient {
address token;
address owner;
IUniswapExchange public exchange;
IShifterRegistry public registry;
constructor(IUniswapExchange _exchange, IShifterRegistry _registry, address _owner) public {
exchange = _exchange;
registry = _registry;
token = exchange.tokenAddress();
owner = _owner;
}
// GSN Support
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 {
}
...
}

Next, we’ll create the swap function, which your API will call to execute a trade for a given user.

function swap(
uint256 _amount,
address payable _to
) external payable
returns (uint256 ethBought)
{
// Only owner can swap
require(_msgSender() == owner);
// Approve and trade the shifted tokens with the uniswap exchange.
require(IERC20(token).approve(address(exchange), _amount));
ethBought = exchange.tokenToEthSwapInput(_amount, uint256(1), uint256(block.timestamp * 2));
// Send proceeds to the User
_to.transfer(ethBought);
}

Finally, we’ll create a function that will add to shifted funds from the user to the contract’s balance.

function shiftIn(
bytes calldata _msg,
uint256 _amount,
bytes32 _nHash,
bytes calldata _sig
) external {
// Shift in and keep tokens in the contract
bytes32 pHash = keccak256(abi.encode(_msg));
registry.getShifterByToken(token).shiftIn(pHash, _amount, _nHash, _sig);
}

The REST API

We’ll create a simple Node.js API that users will communicate with, monitor blockchain networks, and execute trades.

A trade will start with the user requesting a gateway address that the user will need to send funds to in order to initiate the trade.

const RenJS = require("@renproject/ren");
const { fromConnection } = require('@openzeppelin/network/lib')
const express = require('express');
const router = express.Router();
...
router.post('/swap-gateway/create', function(req, res, next) {
const params = req.body
const amount = params.sourceAmount
const dest = params.destinationAddress
const shiftIn = ren.shiftIn({
sendToken: RenJS.Tokens.BTC.Btc2Eth,
sendAmount: Math.floor(amount * (10 ** 8)), // Convert to Satoshis
sendTo: adapterAddress,
contractFn: "shiftIn",
contractParams: [
{
name: "_msg",
type: "bytes",
value: web3Context.lib.utils.fromAscii(`Depositing ${amount} BTC`),
}
],
});
const gatewayAddress = shiftIn.gatewayAddress
gatewayStatusMap[gatewayAddress] = {
status: 'pending',
txHash: ''
}
monitorShiftIn(shiftIn, dest)
res.json({ gatewayAddress })
});

Once a gateway is returned to a user, the backend will start listening for funds sent to the address.

// Stagger Swap and Shift-in based on tx confirmations
const monitorShiftIn = async function (shiftIn, dest) {
const gateway = shiftIn.gatewayAddress
const confsTillSwap = 0
const confsTillShiftIn = 2
console.log('awaiting initial tx', gateway, shiftIn.params.sendAmount)
const initalConf = await shiftIn.waitForDeposit(confsTillSwap);
console.log('calling swap', shiftIn.params.sendAmount, dest, gateway)
swap(shiftIn.params.sendAmount, dest, gateway)
console.log('awaiting final confs', gateway)
const fullConf = await shiftIn.waitForDeposit(confsTillShiftIn);
console.log('submitting to renvm', fullConf)
const renvm = await fullConf.submitToRenVM()
console.log('renvm response', renvm)
completeShiftIn(shiftIn, renvm.signature, renvm.response)
}

Once the transaction has 0 confirmations, the API will call swap() on the adapter.

// Swap using contract funds
const swap = async function (amount, dest, gateway) {
console.log('swap amount', amount, dest)
const adapterContract = new web3Context.lib.eth.Contract(adapterABI, adapterAddress)
try {
const result = await adapterContract.methods.swap(
amount,
dest
).send({
from: web3Context.accounts[0]
})
// console.log('result', result)
gatewayStatusMap[gateway].status = 'complete'
gatewayStatusMap[gateway].txHash = result.transactionHash
} catch(e) {
console.log(e)
}
}

Once 2 confirmations are reached, the API will call completeShiftIn() on the adapter to make the contract’s balance whole.

// Complete the shift once RenVM verifies the tx
const completeShiftIn = async function (shiftIn, signature, response) {
console.log('completeShiftIn', signature, response)
const params = shiftIn.params
const msg = params.contractParams[0].value
const amount = params.sendAmount
const nHash = response.args.nhash
const adapterContract = new web3Context.lib.eth.Contract(adapterABI, adapterAddress)
try {
const result = await adapterContract.methods.shiftIn(
msg,
amount,
nHash,
signature
).send({
from: web3Context.accounts[0]
})
console.log('shift in hash for ' + shiftIn.gatewayAddress, result.transactionHash)
} catch(e) {
console.log(e)
}
}

Finally, let’s set up a route for a UI can call to monitor the progress of the trade.

router.get('/swap-gateway/status', function(req, res, next) {
const id = req.query.gateway
res.json(id && gatewayStatusMap[id] ? gatewayStatusMap[id] : {});
});

That’s it! You now have a fast, non-custodial exchange API with access to large public pools of liquidity.

Areas for Improvement and Features

This is the simplest example possible, and there are plenty of things to improve upon. One of the best things we could do for users is eliminate the ability for the REST API to ignore them, never execute the swap, and steal their funds.

To achieve this we’ll make updates that will have the contract keep track if swap has been called for a user. If shiftIn is called and a swap hasn’t been performed, the method will go ahead and execute the swap instead.This allows the user to progress with a swap themselves once the shift in is ready.

First we’ll add a mapping that establishes statuses for each swap made.

mapping(bytes32=>bool) public shiftInStatuses;

Next we’ll update the shiftIn method to verify that shift in has been completed. If it hasn’t, then call the swap method.

function shiftIn(
address payable _swapReciever,
bytes calldata _msg,
uint256 _amount,
bytes32 _nHash,
bytes calldata _sig
) external {
// Anyone can call this method
bytes32 pHash = keccak256(abi.encode(_swapReciever, _msg));
bytes32 signedMessageHash = getSignedMessageHash(pHash, _amount, _nHash);
uint256 shiftedTokens = registry.getShifterByToken(token).shiftIn(pHash, _amount, _nHash, _sig);
// Require a valid shift in
require(shiftedTokens > 0);
// If swap for shift in hasn't been made yet, allow user to swap
if (shiftInStatuses[signedMessageHash] == false) {
require(IERC20(token).approve(address(exchange), _amount));
uint256 ethBought = exchange.tokenToEthSwapInput(_amount, uint256(1), uint256(block.timestamp * 2));
_swapReciever.transfer(ethBought);
shiftInStatuses[signedMessageHash] = true;
}
}

Finally, let’s update the swap method to update a shift-in’s status.

function swap(
uint256 _amount,
address payable _swapReciever,
bytes calldata _msg,
bytes32 _nHash
) external payable
returns (uint256 ethBought)
{
// Only owner can swap
require(msg.sender == owner);
// Approve and trade the shifted tokens with the uniswap exchange.
require(IERC20(token).approve(address(exchange), _amount));
ethBought = exchange.tokenToEthSwapInput(_amount, uint256(1), uint256(block.timestamp * 2));
// Send proceeds to the User
_swapReciever.transfer(ethBought);
// Update shift in shift in status
bytes32 pHash = keccak256(abi.encode(_swapReciever, _msg));
bytes32 signedMessageHash = getSignedMessageHash(pHash, _amount, _nHash);
shiftInStatuses[signedMessageHash] = true;
}

With a couple more lines of solidity, it’s also straightforward to:

  • Add a custom service fee, charged in either source or destination asset

  • Facilitate non-Ethereum asset swaps using Uniswap plus an immediate RenVM shift-out

  • Reducing fraud risk through locking funds in the adapter until shift ins are complete

Check out the entire project repo here.