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 lock and mint 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.
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.
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:
Generate a gateway address for the user that will send funds to your adapter contract
Monitor the source blockchain network for a transaction to the gateway address
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
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 minted assets owned by your wallet
Automatically add locked & minted 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 transfer and replenish the float when a larger number of confirmations is completed
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;IGatewayRegistry public registry;constructor(IUniswapExchange _exchange, IGatewayRegistry _registry, address _owner) public {exchange = _exchange;registry = _registry;token = exchange.tokenAddress();owner = _owner;}// GSN Supportfunction 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 payablereturns (uint256 ethBought){// Only owner can swaprequire(_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 mint(bytes calldata _msg,uint256 _amount,bytes32 _nHash,bytes calldata _sig) external {// Shift in and keep tokens in the contractbytes32 pHash = keccak256(abi.encode(_msg));registry.getGatewayByToken(token).mint(pHash, _amount, _nHash, _sig);}
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.bodyconst amount = params.sourceAmountconst dest = params.destinationAddressconst mint = ren.lockAndMint({asset: "BTC",from: Bitcoin(),to: Ethereum(web3.currentProvider).Contract({sendTo: adapterAddress,contractFn: "mint",contractParams: [{name: "_msg",type: "bytes",value: web3Context.lib.utils.fromAscii(`Depositing ${amount} BTC`),}],nonce: RenJS.utils.randomNonce()})});const gatewayAddress = mint.gatewayAddressgatewayStatusMap[gatewayAddress] = {created: Date.now(),status: 'pending',txHash: ''}monitorMint(mint, 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 confirmationsconst monitorMint = async function (mint, dest) {const gateway = mint.gatewayAddressmint.on("deposit", async (dep) => {swap(mint.params.sendAmount, dest, gateway)await dep.confirmed()const signed = await dep.signed()const query = signed._state.queryTxResultconst out = query.outcompleteShiftIn(mint, out.signature, query)})}
Once the transaction has 0 confirmations, the API will call swap()
on the adapter.
// Swap using contract fundsconst swap = async function (amount, dest, gateway) {gatewaySwapAttemptMap[gateway] = 0// in case transaction is rejected by GSN, retry every 30 secsgatewaySwapIntervalMap[gateway] = setInterval(async () => {// 3 attempts maxgatewaySwapAttemptMap[gateway] = gatewaySwapAttemptMap[gateway] + 1if (gatewaySwapAttemptMap[gateway] > 3) {gatewayStatusMap[gateway].status = 'error'return clearInterval(gatewaySwapIntervalMap[gateway])}const adapterContract = new web3Context.lib.eth.Contract(adapterABI, adapterAddress)const gasPrice = await web3Context.lib.eth.getGasPrice()try {const result = await adapterContract.methods.swap(amount,dest).send({from: web3Context.accounts[0],gasPrice: Math.round(gasPrice * 1.5)})gatewayStatusMap[gateway].status = 'complete'gatewayStatusMap[gateway].txHash = result.transactionHashclearInterval(gatewaySwapIntervalMap[gateway])} catch(e) {console.log(e)gatewayStatusMap[gateway].status = 'error'gatewayStatusMap[gateway].error = e}}, (1000 * 30))}
Once 2 confirmations are reached, the API will call completeMint()
on the adapter to make the contract’s balance whole.
const completeMint = async function (mint, signature, response) {const params = mint.paramsconst msg = params.contractCalls[0].contractParams[0].valueconst amount = params.sendAmountconst nHash = response.nhashconst adapterContract = new web3Context.lib.eth.Contract(adapterABI, adapterAddress)const gasPrice = await web3Context.lib.eth.getGasPrice()try {const result = await adapterContract.methods.mint(msg,amount,nHash,signature).send({from: web3Context.accounts[0],gasPrice: Math.round(gasPrice * 1.5)})} catch(e) {console.log(e)gatewayStatusMap[gateway].status = 'error'gatewayStatusMap[gateway].error = 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.gatewayres.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.
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 mint
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 mintStatuses;
Next we’ll update the mint
method to verify that transfer has been completed. If it hasn’t, then call the swap method.
function mint(address payable _swapReciever,bytes calldata _msg,uint256 _amount,bytes32 _nHash,bytes calldata _sig) external {// Anyone can call this methodbytes32 pHash = keccak256(abi.encode(_swapReciever, _msg));bytes32 signedMessageHash = getSignedMessageHash(pHash, _amount, _nHash);uint256 mintedTokens = registry.getGatewayByToken(token).mint(pHash, _amount, _nHash, _sig);// Require a valid shift inrequire(mintedTokens > 0);// If swap for shift in hasn't been made yet, allow user to swapif (mintStatuses[signedMessageHash] == false) {require(IERC20(token).approve(address(exchange), _amount));uint256 ethBought = exchange.tokenToEthSwapInput(_amount, uint256(1), uint256(block.timestamp * 2));_swapReciever.transfer(ethBought);mintStatuses[signedMessageHash] = true;}}
Finally, let’s update the swap method to update a mint’s status.
function swap(uint256 _amount,address payable _swapReciever,bytes calldata _msg,bytes32 _nHash) external payablereturns (uint256 ethBought){// Only owner can swaprequire(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 statusbytes32 pHash = keccak256(abi.encode(_swapReciever, _msg));bytes32 signedMessageHash = getSignedMessageHash(pHash, _amount, _nHash);mintStatuses[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 burn & release
Reducing fraud risk through locking funds in the adapter until mints are complete