Uniswap Trading Data With Chainlink Functions and The Graph

The full code for this tutorial can be found on GitHub.

In the rapidly changing world of cryptocurrency trading, staying ahead of the curve is crucial. Traders need access to accurate, real-time data as market conditions can shift rapidly. This is where Chainlink Functions and The Graph come in handy.

This tutorial will teach you how to utilize the potential of The Graph’s subgraphs, Uniswap router, and Chainlink Functions to implement a dynamic trading strategy.

Understanding Liquidity and Its Significance

The trading ecosystem thrives on liquidity, representing the ease of buying or selling assets without causing significant price fluctuations. Assets with high liquidity are easier to trade and tend to be less volatile in price, making them an attractive pick for traders seeking stability and reliability in their portfolios.

This tutorial focuses on the idea that stronger liquidity implies relatively safer assets. By utilizing liquidity data from Uniswap pools, we can extract valuable insights into the risk associated with asset swaps. With this knowledge at our disposal, we can make more informed trading decisions and better manage our risks.

Chainlink Functions are essential to our trading strategy as they allow us to access and analyze Uniswap V3 WETH-USD pool data through a subgraph, an open API that organizes and serves blockchain data. Specifically, we utilize them to retrieve three-day trading volume data, a crucial metric for assessing liquidity.

If our analysis indicates an increase in liquidity for the WETH-USD pair, we will trigger a Chainlink Function within FunctionsConsumer.sol. This function will enable us to swap Wrapped Matic in our contract balance to Wrapped Ether, taking advantage of the improving liquidity conditions.

Tailoring the Strategy to Your Needs

We focus on the WETH-USD pair in the Polygon Mumbai testnet for testing purposes. However, this strategy’s beauty lies in its flexibility. In a production environment, you can choose any token pair that aligns with your trading needs and strategy. The principles and tools discussed here can be applied to various cryptocurrency assets.

Whether you’re a seasoned trader looking to refine your approach or a newcomer seeking a reliable method for cryptocurrency trading, this tutorial is your gateway to leveraging Chainlink Functions and Uniswap data via The Graph for informed decisions. Let’s dive in and unlock the potential of data-driven trading.

Step 1: Setting Up Your Environment

Before we dive into the intricacies of connecting The Graph and Uniswap router using Chainlink Functions, let’s ensure your environment is ready. Here’s a quick rundown of the initial setup:

  1. Ensure you have the following requirements installed on your computer:
    1. Node.js version 18.18.0 (or the latest release of Node.js v18 if a later one is available)
    2. Deno version 1.36 (or the latest release of Deno v1 if a later one is available)
  2. Clone the Repository: Begin by cloning the repository to your local machine. This will give you access to all the necessary code and files to implement the trading strategy.
  3. Install Dependencies: Once you’ve cloned the repository, open the directory in your command line and run npm install to install all the required dependencies.
  4. Github Personal Access Token: To interact with Gists on GitHub, you’ll need a personal access token. Go to GitHub’s token generation page and create a new token. Name it and enable read & write access for Gists. Make sure not to allow any additional permissions. Record the generated token; you’ll need it in just a moment.
  5. The Graph Network API Key: Your trading strategy relies on data indexed by subgraphs, so you’ll need an API key for The GraphNetwork. Be sure to claim queries and check that FREE REMAINING: 1000 is displayed in your key’s QUERIES TOTAL column.
  6. Setting Environment Variables: To keep your sensitive information secure, set up encrypted environment variables with env-enc. Start by setting an encryption password by running npx env-enc set-pw. Then, use the npx env-enc set command to set the required environment variables:
    1. GITHUB_API_TOKEN: Paste your GitHub token from step 3.
    2. GRAPH_KEY: Add your Graph key obtained in step 4.
    3. PRIVATE_KEY: Include your development wallet’s private key.
    4. POLYGON_MUMBAI_RPC_URL: Acquired from an RPC Provider.
    5. Optionally, you can set POLYGONSCAN_API_KEY for contract verification.

Step 2: Understanding the Key Files

When implementing this trading strategy, two essential files are central:

  • contracts/FunctionsConsumer.sol: This smart contract receives data and executes the swap based on your trading strategy.
  • graph_request.js: This JavaScript code is executed by each node of the Decentralized Oracle Network (DON) to facilitate data requests and fulfillments.

Let’s start by taking a look at graph_request.js. This JavaScript code is executed by each node of the Decentralized Oracle Network (DON) to facilitate data requests and fulfillments. Let’s break down its functionality step by step.

Setting Up The Request

const graphKey = secrets.graphKey;
const graphRequest = Functions.makeHttpRequest({
url: `https://gateway-arbitrum.network.thegraph.com/api/${graphKey}/subgraphs/id/HUZDsRpEVP2AvzDCyzDHtdc64dyDxx8FQjzsmqSg4H3B`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: {
query: `{
poolDayDatas(
first: 3
orderBy: date
orderDirection: desc
where: { pool: "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640" }
) {
id
liquidity
date
volumeUSD
tick
}
}`,
},
});

This code snippet is where the action begins. Let’s break it down:

  • graphKey: We start by retrieving the key for The Graph Network, which is essential for authentication and authorization. This key is stored via env-enc and retrieved via the secrets object.
  • graphRequest: Next, we create an HTTP POST request using Functions.makeHttpRequest. This request is directed to The Graph API.
    • url: The URL for the request is dynamically constructed, incorporating the graphKey into the URL. This URL points to the specific subgraph that contains the data we need.
    • method: We specify that this is a POST request.
    • headers: We set the “Content-Type” to “application/json” to indicate that we are sending JSON data.
    • data: Inside the data field, we define a GraphQL query. This query fetches data from the poolDayDatas of a particular pool identified by its address.

Handling the Response

const [graphResponse] = await Promise.all([graphRequest]);
let liquidities = [];
if (!graphResponse.error) {
for (let i = 0; i < 3; i++) {
liquidities.push(graphResponse.data.data.poolDayDatas[i].liquidity);
}
} else {
console.log("graphResponse Error, ", graphResponse);
}

Once we’ve sent the request, we await the response and store it in graphResponse. Here’s what happens next:

  • We initialize an empty array called liquidities to store liquidity data.
  • We check for no error in the response (!graphResponse.error). If there’s no error, we extract liquidity data from the response.
  • In a loop, we iterate over the first 3 entries in the response data and push the liquidity values into the liquidities array.
  • If there is an error in the response, we log an error message to the console.

Liquidity Analysis and Return Value

// check if liquidity is increasing
// if it is increasing, return 1, and FunctionsConsumer triggers the function to swap WMATIC to WETH.
// if it is not, return 0, and FunctionsConsumer does nothing.
if (liquidities[0] > liquidities[1] && liquidities[0] > liquidities[2]) {
return Functions.encodeUint256(0);
}
return Functions.encodeUint256(1);

Finally, we perform liquidity analysis based on the data we’ve collected:

  • We check whether the liquidity of the most recent entry (liquidities[0]) is greater than the liquidity of the previous two entries. If it is, Then the liquidity is NOT increasing, and we return Functions.endcodedUint256(0)
  • Otherwise, we return Functions.encodeUint256(1)
  • This return value triggers the function to swap WMATIC to WETH.

    Next, let’s take a look at contracts/FunctionsConsumer.sol

Contract Structure and Imports

pragma solidity ^0.8.19;
import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol";
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import {FunctionsClient} from "./@chainlink/contracts/src/v0.8/functions/dev/1_0_0/FunctionsClient.sol";
import {ConfirmedOwner} from "./@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {FunctionsRequest} from "./@chainlink/contracts/src/v0.8/functions/dev/1_0_0/libraries/FunctionsRequest.sol";
contract FunctionsConsumer is FunctionsClient, ConfirmedOwner {
// Contract variables and constructor
}

The contract starts with version pragma and imports several libraries and contracts essential for its functionality:

  • TransferHelper.sol and ISwapRouter.sol from the Uniswap V3 Periphery provide utility functions for token transfers and access to the Uniswap router.
  • FunctionsClient and ConfirmedOwner contracts are imported from the Chainlink contracts. FunctionsClient provides interaction functionality with Chainlink Functions, and ConfirmedOwner restricts certain functions to the contract owner.
  • FunctionsRequest.sol contains utilities for creating and managing Chainlink Functions requests.

Variable Initialization

bytes32 public donId; // DON ID for the Functions DON to which the requests are sent
bytes32 public s_lastRequestId;
bytes public s_lastResponse;
bytes public s_lastError;
ISwapRouter public immutable swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
address public constant WETH = 0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa;
address public constant WMATIC = 0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889;
uint24 public constant poolFee = 3000;

Let’s break down what each of these variables represents:

donId: This is the DON ID (Decentralized Oracle Network ID) to which the Chainlink Functions requests are sent.

s_lastRequestId: Used to store the last Chainlink Functions request ID generated by the contract. Request IDs are unique identifiers for Chainlink Functions requests and are used to track and retrieve the responses for specific requests.

s_lastResponse stores the last response received from a Chainlink Functions request.

s_lastError: Used to store the last error message received from a Chainlink Functions request. If there are issues with a request, such as a failure to fetch data or execute a function, error messages can provide diagnostic information.

swapRouter: Represents the Uniswap router contract address. The contract uses this router to execute token swaps on the Uniswap decentralized exchange.

WETH and WMATIC: Store the addresses of Wrapped Ether (WETH) and Wrapped Matic (WMATIC) tokens, respectively. These addresses are constants used within the contract to specify the tokens involved in token swaps.

poolFee: Represents the pool fee associated with Uniswap V3 liquidity pools. It is set to a constant value of 3000, corresponding to a 0.3% fee on trades in these pools.

Contract Initialization

constructor(address router, bytes32 _donId) FunctionsClient(router) ConfirmedOwner(msg.sender) {
donId = _donId;
}

The contract is initialized in the constructor with a Functions router address and a DON (Decentralized Oracle Network) ID.

function setDonId(bytes32 newDonId) external onlyOwner {
donId = newDonId;
}

This function allows the contract owner to update the DON ID. The DON ID refers to the specific Chainlink DON to which requests are sent, and it can be modified as needed.

function sendRequest(
string calldata source,
FunctionsRequest.Location secretsLocation,
bytes calldata encryptedSecretsReference,
string[] calldata args,
bytes[] calldata bytesArgs,
uint64 subscriptionId,
uint32 callbackGasLimit
) external onlyOwner {
FunctionsRequest.Request memory req;
req.initializeRequest(FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, source);
req.secretsLocation = secretsLocation;
req.encryptedSecretsReference = encryptedSecretsReference;
if (args.length > 0) {
req.setArgs(args);
}
if (bytesArgs.length > 0) {
req.setBytesArgs(bytesArgs);
}
s_lastRequestId = _sendRequest(req.encodeCBOR(), subscriptionId, callbackGasLimit, donId);
}

The sendRequest function is used to trigger on-demand Chainlink Functions requests. It accepts several parameters:

  • source: The JavaScript source code defining the function to be executed.
  • secretsLocation: The location of secrets (either remote or DON-hosted).
  • encryptedSecretsReference: A reference to encrypted secrets.
  • args and bytesArgs: String and bytes arguments passed into the source code.
  • subscriptionId: The subscription ID used to pay for the request.
  • callbackGasLimit: The maximum gas limit for calling the fulfillRequest function.
function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override {
s_lastResponse = response;
s_lastError = err;
uint256 liquidityDrop = uint256(bytes32(response));
if (liquidityDrop == 1) {
swapExactInputSingle();
}
}

The fulfillRequest function handles the response from the Chainlink Functions request. This implementation stores the response and error and then checks if the response indicates a liquidity drop. If a liquidity drop is detected, it triggers a Uniswap swap using the swapExactInputSingle function.

Swapping Tokens on Uniswap

function swapExactInputSingle() internal returns (uint256 amountOut) {
uint256 amountIn = checkBalance();
require(amountIn > 0, "Please transfer some Wrapped MATIC to the contract");
// Approve the router to spend WMATIC.
TransferHelper.safeApprove(WMATIC, address(swapRouter), amountIn);
// Naively set amountOutMinimum to 0. In production, use an oracle or other data source to choose a safer value for amountOutMinimum.
// We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount.
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: WMATIC,
tokenOut: WETH,
fee: poolFee,
recipient: address(this),
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
// The call to `exactInputSingle` executes the swap.
amountOut = swapRouter.exactInputSingle(params);
}

The swapExactInputSingle function executes a token swap on Uniswap using the specified parameters. It first checks the contract’s WMATIC balance, ensuring sufficient tokens for the swap. Then, it sets the parameters for the swap, including the token input, token output, fee, and other details. Finally, it calls the Uniswap router’s exactInputSingle function to perform the swap.

Checking Token Balance

function checkBalance() internal view returns (uint256 balance) {
IERC20 wrappedMatic = IERC20(WMATIC);
balance = wrappedMatic.balanceOf(address(this));
}

The checkBalance function is a helper function used to check the WMATIC balance of the contract. It ensures that there are enough tokens available for the swap.

Step 3: Testing Locally

Before deploying your strategy to a live blockchain network, it’s wise to test it locally. You can simulate an end-to-end request and fulfillment by running:

npx hardhat functions-simulate-script

Step 4: Deploying to the Blockchain

Once satisfied with your local tests, it’s time to take your strategy live. Deploy and verify the client contract on an actual blockchain network by running:

npx hardhat functions-deploy-consumer --network polygonMumbai --verify true
  • *Note that you should have POLYGONSCAN_API_KEY set if you use -verify true, depending on your network.

Step 5: Setting Up Billing

You must create, fund, and authorize a new Functions billing subscription to use Chainlink Functions. Run the following command to achieve this:

npx hardhat functions-sub-create --network polygonMumbai --amount 5 --contract <contract_address>

Here are a few things to keep in mind:

  • Make sure to sign the term of use in the Chainlink Functions app if this is the first time you are using. Please head to the Functions app, create a new subscription, and sign the terms of use. See the documentation for more information.
  • Ensure your wallet has at least 5 LINK tokens in your balance before running this command. You can obtain testnet LINK tokens at faucets.chain.link.
  • Confirm that you’re using the Polygon Mumbai testnet, and if you plan to use the contract on other networks, adjust the network conditions accordingly. You’ll need to modify the FunctionsConsumer.sol file to have a different value for the line if (block.chainid != 80001).

Step 6: Preparing for the Swap

Before initiating the swap, transfer at least 0.1 WMATIC to the FunctionsConsumer contract. Ensure you have WMATIC in your Metamask wallet. The address for WMATIC on Polygon Mumbai is 0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889.

MATIC tokens for the Mumbai testnet can be obtained at the Alchemy faucet, and MATIC can be swapped to Wrapped Matic (WMATIC) at Uniswap on Mumbai.

Step 7: Making an On-Chain Request

The moment of truth has arrived. Make an on-chain request to kickstart your trading strategy:

npx hardhat functions-request --network polygonMumbai --contract <contract_address> --subid <subId> --callbackgaslimit 300000

Ensure you include the parameter --gaslimit and set it to 300,000.

With these steps completed, you are well on your way to implementing a data-driven trading strategy that leverages Chainlink Functions and Uniswap data.


In this tutorial, we have discovered how Chainlink Functions and The Graph can help traders create a dynamic and data-driven strategy. By understanding the importance of liquidity and analyzing Uniswap pool data, we can make informed decisions and manage risks more effectively. Chainlink Functions have played a crucial role in this process by allowing us to take actions based on liquidity changes. Whether you’re an experienced trader or new to the game, this tutorial provides you with the necessary tools to utilize data for smarter trading. Take advantage of the flexibility of this strategy, personalize it to fit your needs, and explore the world of cryptocurrency trading powered by data.


Category
Graph Builders
Author
Richard Gottleber
Published
December 8, 2023

Richard Gottleber

View all blog posts