Building Your First Uniswap Integration

August 25, 2022

Developers who are new to the Uniswap ecosystem often get stuck in the first stages of developing on-chain integrations with the Protocol. In this post, we’re going to help you get over those first hurdles. Here’s what we’ll accomplish:

  1. Set up a basic, reusable development environment to build and test smart contracts that interact with the Uniswap Protocol
  2. Run a local development Ethereum node forked from Mainnet that includes the full Uniswap Protocol
  3. Write a basic smart contract that executes a swap on Uniswap V3
  4. Deploy and test that contract

Setting up a Development Environment

One of the most common questions we get asked is what development stack to use to build on Uniswap. There isn’t one right answer to this, but for this post we’ll use a common stack: Hardhat for our main toolchain which runs nodes and provides a test framework and Alchemy for our RPC provider (which supports Mainnet forking in its free tier).

Set Up an Alchemy Account

RPCs are services that allow off-chain clients to communicate with the blockchain, a requirement for building any sort of dApp. For this example we’re going to use Alchemy, which supports a Mainnet Fork which is key for our development setup.

Head over to https://www.alchemy.com/ to create a free account. Click ‘Create an App,’ follow the prompts, and grab your API key. We’ll need that later.

Cloning the Sample Project

To get up and running quickly, we’ll start by cloning the reusable boilerplate repo that Uniswap Labs has provided by navigating into a directory for your project and using git:

git clone https://github.com/Uniswap/uniswap-first-contract-example

The repo is very similar to how Hardhat would set up a default environment, with a few files stubbed out for us to build on. Navigate into the cloned project and install its dependencies:

cd uniswap-first-contract-example
npm install

You should now have the Hardhat test environment installed along with some other dependencies. Hardhat will be running a fork on Mainnet for you, handling deployments to that node and running tests against it. We won’t go too deep into Hardhat, but if you’re not familiar, we highly encourage reading more here.

Forking Mainnet

Hardhat has a powerful feature that runs a test Ethereum node locally for you to develop on. This saves you the gas costs of testing in production and the headaches of developing against a testnet.

However, one problem with developing on a local node is that it starts from scratch each time it boots it up. No deployed protocols (like Uniswap) will be present on it, which makes it difficult to build and test integrations.

This is where a Mainnet Fork comes in. When we use a Mainnet Fork, Hardhat actually takes a snapshot of the Ethereum Mainnet and uses that as the starter for your local node. That way all deployed protocols are present and ready to be integrated with.

From here, it’s time to use the Alchemy API key we generated earlier. Run the following command to start your local Ethereum test node, forked from a snapshot of Mainnet:

npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/{YOUR_API_KEY}

Congratulations! You are now running a fully functional test Ethereum node on your local machine. And since we started from a fork Mainnet, your local node contains the complete history of the blockchain, including the current state of the Uniswap Protocol. We’re now ready to build an integration.

Writing a Basic Swap Contract

For this example, we’ll start by building a basic integration with the Uniswap V3 Protocol. Our contract will take in an input amount of Wrapped Ether, swap it for the maximum amount of DAI, and return that DAI to the caller’s wallet.

Let’s begin by opening up the contracts/SimpleSwap.sol file.

We’ve provided some boiler plate to get the contract set up. Let’s walk through it:

/* contracts/Swap.sol */

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.7.6;
pragma abicoder v2;

import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';

contract SimpleSwap {
    constructor() {
    }
}

The first three lines are standard in Solidity programming, they set up the license and compiler info. We won’t go too deep into what they mean here, but if you’re not familiar with what these do we highly recommend reading through the Solidity language docs.

Next, we import two elements from the Uniswap V3 Protocol (these got imported when we ran npm install earlier). The first — ISwapRouter.sol — is the interface of the Uniswap SwapRouter contract, which we’ll use to route our swap to the appropriate contract methods on the Protocol. The second — TransferHelper.sol — is a utility library to help us do some required operations on ERC20 tokens. We’ll use these later in our contract.

Finally, we define our contract called SimpleSwap. This contract doesn’t do anything right now, but we can add some code so that it will perform a simple swap.

Swap Code Setup

We’ll start by adding some constant addresses and references. In a production version of this contract, these should be dynamic (a great follow up project) — but to keep things simple we’re going to hard code them.

Add the following lines to your contracts/SimpleSwap.sol file:

// ...
contract SimpleSwap {
    ISwapRouter public immutable swapRouter;
    address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    uint24 public constant feeTier = 3000;

        constructor(ISwapRouter _swapRouter) {
        swapRouter = _swapRouter;
    }
}

First we use the ISwapRouter interface to create a reference to a SwapRouter. The SwapRouter is part of the Uniswap V3 Periphery contracts, designed to make executing swaps easier. We’ll actually map this to a deployed version of the SwapRouter in the constructor:

ISwapRouter public immutable swapRouter;

Second, we create constant variables for the ERC20 tokens that we’ll be swapping. You can and should put these addresses into Ether Scan and confirm that they are the tokens you’re expecting:

address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

Next we create another constant variable to indicate the fee tier of the pool we want to use to swap. Again, in a more general contract this would be an input that gets set at runtime, but for simplicity we’re hardcoding to the 0.3% fee tier pool. Fee tiers are denoted in 1/100ths of a basis point so our fee tier will be 3000:

uint24 public constant feeTier = 3000;

Finally, we create the constructor, which gets called when our contract is deployed. We’re requiring that an integrator pass in the address of the SwapRouter that they want to use. All the constructor does then is set the swapRouter to use the one provided by the integrator:

constructor(ISwapRouter _swapRouter) {
    swapRouter = _swapRouter;
}

And that’s it! Now that we have all of the set up code written, we can move on to actually executing our swap.

Swap Code Implementation

Again, this sample contract only does one thing: swap WETH for DAI. Let’s start by creating the function signature for our swap.

contract SimpleSwap {
    //...
    function swapWETHForDAI(uint amountIn) external returns (uint256 amountOut) {
        // We'll fill this in next
        }
}

Our function swapWETHForDAI, is marked external so clients and other contracts can call it. The function takes in one parameter, the amount of WETH we want to swap denominated in Wei (which is 10^-18 WETH). OpenZeppelin has a good explanation of why we denominate in Wei as opposed to WETH (TLDR; it allows us to do accurate arithmetic in Solidity which doesn’t support floating point numbers). For the purposes of this project, just know to swap 1 WETH you have to pass this function an amountIn of 1*10^18.

Performing a swap from WETH to DAI will require two prerequisite steps. First, our contract will have to move the requested amount of WETH from the caller’s wallet to itself and then it will need to approve the swapRouter to spend that WETH to swap for DAI. It sounds complicated, but you’ll see the Uniswap V3 Periphery contracts provide us tools to make it easy. Start by adding the following lines to your swapWETHForDAI function:

function swapWETHForDAI(uint amountIn) external returns (uint256 amountOut) {
    // Transfer the specified amount of WETH9 to this contract.
    TransferHelper.safeTransferFrom(WETH9, msg.sender, address(this), amountIn);

        // Approve the router to spend WETH9.
    TransferHelper.safeApprove(WETH9, address(swapRouter), amountIn);
}

The first step calls the periphery helper function safeTransferFrom, which transfers the desired amount of WETH from the caller’s wallet into the contract. Keep in mind that since we’re transferring on behalf of the user, that user will have to sign an approval before calling this method. This is a critical concept to understand, and we’ll go through how to do that in the next section when we create a test client.

The next line calls safeApprove to allow the swapRouter to spend the specified amount of WETH. This will give SwapRouter permission to actually execute the swap for us. With that, we’re finally ready to swap!

Remember how we said that the Swap Router makes it easy for us to actually execute a swap? Here’s where we’ll use it. We’ll call the exactInputSingle method, which will run a swap of an “exact amount” of an input token for the maximum amount of an output token.

Add the following code to the swapWETHForDAI function to execute the swap. It may seem complicated, but we’ll step through it.

function swapWETHForDAI(uint amountIn) external returns (uint256 amountOut) {
    // ...
    ISwapRouter.ExactInputSingleParams memory params =
      ISwapRouter.ExactInputSingleParams({
          tokenIn: WETH9,
          tokenOut: DAI,
          fee: feeTier,
          recipient: msg.sender,
          deadline: block.timestamp,
          amountIn: amountIn,
          amountOutMinimum: 0,
          sqrtPriceLimitX96: 0
      });
  // The call to `exactInputSingle` executes the swap.
  amountOut = swapRouter.exactInputSingle(params);
  return amountOut;
}

We’ll start by setting up the parameters required to run the exactInputSingle method. Those who are new to Solidity will notice the memory keyword next to the variable declaration. This just tells the compiler to store this variable locally during execution as opposed to writing it to the blockchain, which would be expensive (learn more here).

Once again the SwapRouter Interface makes our lives easier here by giving us the exact form of the parameter that the swap function needs with ExactInputSingleParams. All but the last two elements of this object should be pretty self explanatory, we’re just mapping variables that we already set to the parameter object.

For simplicity in this example, we’ll set the last two elements amountOutMinimum and sqrtPriceLimitX96 to zero. These are out of scope for this basic example, but they essentially let you set a minimum amount of the output, in this case DAI, that you’ll receive for a swap. In production, this is one way to limit price slippage from a swap.

Finally, in the last two lines, we’ll execute the exactInputSingle method of the SwapRouter, with the parameters we set up which actually executes the trade, then return the amount of DAI that that trade netted.

Complete Contract

That’s it! You now have a working contract that will swap an inputted amount of WETH for the maximum amount of DAI given current market prices. Before moving to testing, double check that your contract matches the example shown here.

Testing our Contract

Now that we have our SimpleSwap contract written, it’s time to use it. We’ll use the Chai testing framework that ships with Hardhat to:

  1. Deploy our SimpleSwap contract to a fork of Ethereum Mainnet
  2. Check our test wallet’s balance of DAI
  3. Call the swapWETHForDAI to swap some test WETH for DAI
  4. Confirm that the test user’s DAI balance actually increased

Writing tests for contracts could (and likely will) be the content of an entire post. To keep things simple, we’re going to gloss over a lot of details here. After completing this example, we highly encourage doing some outside learning on writing tests for contracts (the Hardhat Testing Docs are a great place to start).

Contract Test File

To get started head over to tests/SimpleSwap.test.js, where we have the code to test our contract stubbed out.

The file starts by importing the test framework Chai and Hardhat, which we’ve been using for our development environment and sets some familiar constants. The ERC-20 ABI snippet lets us call functions like Approve on ERC-20 tokens (read more about ABIs here).

With the setup code out of the way, let’s jump into the the actual test, which starts on line 20. Add the following code to deploy our SimpleSwap contract:

/* Deploy the SimpleSwap contract */
const simpleSwapFactory = await ethers.getContractFactory('SimpleSwap')
const simpleSwap = await simpleSwapFactory.deploy(SwapRouterAddress)
await simpleSwap.deployed()

Hardhat is doing a lot of leg work for us here. It’s deploying the contract to our local environment and saving a callable version of it — simpleSwap — which we’ll be able to execute methods on.

The Hardhat local node provides us an account that is preloaded with a bunch of test ETH. Since we’re swapping WETH for DAI, we have to take some of that ETH and wrap it. Add the following code to first get the keys to the account, stored in the signers variable, then deposit some of it’s ETH in the WETH contract to wrap it:

/* Connect to WETH and wrap some eth  */
let signers = await hre.ethers.getSigners()
const WETH = new hre.ethers.Contract(WETH_ADDRESS, ercAbi, signers[0])
const deposit = await WETH.deposit({ value: hre.ethers.utils.parseEther('10') })
await deposit.wait()

If all goes well, these steps will give us 10 WETH ready to be swapped for DAI. To begin the test, we’ll grab our fake account’s current balance of DAI and save it to the local variable DAIBalanceBefore. You’ll notice we use the formatUnits utility from Ethers to convert the DAI amount to a readable floating point number:

/* Check Initial DAI Balance */
const DAI = new hre.ethers.Contract(DAI_ADDRESS, ercAbi, signers[0])
const expandedDAIBalanceBefore = await DAI.balanceOf(signers[0].address)
const DAIBalanceBefore = Number(hre.ethers.utils.formatUnits(expandedDAIBalanceBefore, DAI_DECIMALS))

Next is a critical, often overlooked, step. Our SimpleSwap contract is going to be moving WETH on out of our wallet and onto the swap contract. It can’t do this without our approval, which we give by calling the approve method on the WETH ERC-20 contract itself. For this example, we’ll approve it to move 1 WETH on our behalf:

/* Approve the swapper contract to spend WETH for me */
await WETH.approve(simpleSwap.address, hre.ethers.utils.parseEther('1'))

And finally it’s the moment we’ve all been waiting for, time to actually execute a swap from WETH to DAI using our brand new SimpleSwap contract! Hardhat makes it super easy, we can just call the swapWETHForDAI method on our contract object, requesting a swap of 0.1 WETH for DAI:

/* Execute the swap */
const amountIn = hre.ethers.utils.parseEther('0.1')
const swap = await simpleSwap.swapWETHForDAI(amountIn, { gasLimit: 300000 })
swap.wait()

Once that transaction completes, we’ll do another check of our account’s DAI balance:

/* Check DAI end balance */
const expandedDAIBalanceAfter = await DAI.balanceOf(signers[0].address)
const DAIBalanceAfter = Number(hre.ethers.utils.formatUnits(expandedDAIBalanceAfter, DAI_DECIMALS))

Finally, compare it to the DAI balance we checked before the swap. If the swap worked, we should now have more DAI than when we started:

/* Test that we now have more DAI than when we started */
expect(DAIBalanceAfter).is.greaterThan(DAIBalanceBefore)

That’s all you need to test your new smart contract. Double check what you have against the complete example here and when you’re ready you can use Hardhat to run the test and see if it worked. To run the test, open a new command line to the repo’s root and run the following command:

npx hardhat test

The test failed! What could have possibly gone wrong?

Error: call revert exception [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (method="balanceOf(address)", data="0x", errorArgs=null, errorName=null, errorSignature=null, reason=null, code=CALL_EXCEPTION, version=abi/5.6.4)
  at Logger.makeError (node_modules/@ethersproject/logger/src.ts/index.ts:261:28)
  at Logger.throwError (node_modules/@ethersproject/logger/src.ts/index.ts:273:20)
  at Interface.decodeFunctionResult (node_modules/@ethersproject/abi/src.ts/interface.ts:427:23)
  at Contract.<anonymous> (node_modules/@ethersproject/contracts/src.ts/index.ts:400:44)
  at step (node_modules/@ethersproject/contracts/lib/index.js:48:23)
  at Object.next (node_modules/@ethersproject/contracts/lib/index.js:29:53)
  at fulfilled (node_modules/@ethersproject/contracts/lib/index.js:20:58)

Using our Fork of Mainnet

When you run a Hardhat test with no parameters, Hardhat spins up a fresh Ethereum test node for your test to run on. The problem? Our contract makes a call to the deployed Uniswap Protocol contracts, which aren’t present on a fresh set up.

Remember how we started a “Mainnet Fork” node in the first section? This is where that comes in handy. Since it’s a fork of Mainnet, it has a deployed version of the Uniswap contracts ready to use. Make sure that node you started is still running (if it’s not, just repeat the steps in the “Forking Mainnet” section) and we’ll re-run the test explicitly pointing Hardhat at our local node:

npx hardhat test --network localhost

You should now see a message like the following indicating your contract is working. Congratulations!

SimpleSwap
    ✔ Should provide a caller with more DAI than they started with after a swap (1999ms)

  1 passing (2s)

What comes next?

You can keep building with this environment, or clone the repo again to start fresh. To continue learning, try adding some more advanced functions:

  • Build a simple front end: The provided test demonstrate how to use Javascript to interact with your contract. Can you move that logic to a Simple Swap website?
  • Add an Exact Output swap function to SimpleSwap: Right now our contract takes in a quantity of WETH and swaps for the maximum amount of DAI. This new method should take in an amount of DAI and swap the correct amount of WETH to get that.
  • Write a GeneralSwap contract: Our contract was hard coded to only swap WETH for DAI. Can you write a contract that can swap between any ERC-20 pairs?
  • Write a Quote contract: Create a new contract that gets current prices for swaps without actually performing a swap.
  • Deploy your contract to a test net: Right now your contract is only deployed on your test node, can you deploy it to a test net like Goerli?
  • Get creative: You now have access to the deepest source of liquidity on Ethereum. Create your own use case and build it.

A great place to start for all of this is the Uniswap Docs site. If you get stuck and need help, post in the #dev-chat channel of the Uniswap Discord.

Until next time, happy building!


To get involved and stay up to date:

Uniswap Team 🦄

    Setting up a Development Environment
    Set Up an Alchemy Account
    Cloning the Sample Project
    Forking Mainnet
    Writing a Basic Swap Contract
    Swap Code Setup
    Swap Code Implementation
    Complete Contract
    Testing our Contract
    Contract Test File
    Using our Fork of Mainnet
    What comes next?