Using the new Uniswap v2 in your contracts
What's new in Uniswap v2 and how to integrate Uniswap v2
Note: For Uniswap 3 check out the tutorial here.
What is UniSwap?
If you're not familiar with Uniswap yet, it's a fully decentralized protocol for automated liquidity provision on Ethereum. An easier-to-understand description would be that it's a decentralized exchange (DEX) relying on external liquidity providers that can add tokens to smart contract pools and users can trade those directly.
Since it's running on Ethereum, what we can trade are Ethereum ERC-20 tokens. For each token there is its own smart contract and liquidity pool. Uniswap - being fully decentralized - has no restrictions to which tokens can be added. If no contracts for a token pair exist yet, anyone can create one using their factory and anyone can provide liquidity to a pool. A fee of 0.3% for each trade is given to those liquidity providers as incentive.
The price of a token is determined by the liquidity in a pool. For example if a user is buying TOKEN1 with TOKEN2, the supply of TOKEN1 in the pool will decrease while the supply of TOKEN2 will increase and the price of TOKEN1 will increase. Likewise, if a user is selling TOKEN1, the price of TOKEN1 will decrease. Therefore the token price always reflects the supply and demand.
And of course a user doesn't have to be a person, it can be a smart contract. That allows us to add Uniswap to our own contracts for adding additional payment options for users of our contracts. Uniswap makes this process very convenient, see below for how to integrate it.
What is new in UniSwap v2?
- ERC20 / ERC20 Pairs: In the first version any token had to be paired with ETH. To trade a token with another token, one had to first exchange the first token into ETH and then use that ETH to purchase the other token. Now you can directly trade those!
- Price Oracles: While theoretically one could use Uniswap v1 as an oracle, it wasn't recommended as prices could flucuate by a lot in a short time making it easy to manipulate. Now there are several mechanisms to prevent this like using the last block price and a cumulative-price that is weighted by the time previous prices existed.
- Flash Swaps: The Uniswap Flash Swaps are similar to the Aave Flash Loans that you might be familiar with. The same concept is now possible in Uniswap allowing you to optimistically receive tokens as long as you either 1. return them 2. pay for them or 3. partially return/pay for them at the end of the transaction.
- Some Minor Technical Improvements
- Path to Sustainability: An option was added that once activated adds a 0.05% trading fee intended for protocol governance. It's not planned to activate this in the near future.
Further Uniswap v2 resources
What happens to UniSwap v1?
Integrating UniSwap v2
One of the reasons Uniswap is so popular may be the simple way of integrating them into your own smart contract. Let's say you have a system where users pay with DAI. With Uniswap in just a few lines of code, you could add the option for them to also pay in ETH. The ETH can be automatically converted into DAI before the actual logic. It would look something like this
function pay(uint paymentAmountInDai) public payable {
if (msg.value > 0) {
convertEthToDai(paymentAmountInDai);
} else {
require(daiToken.transferFrom(msg.sender, address(this), paymentAmountInDai);
}
// do something with that DAI
...
}
A simple check at the beginning of your function will be enough. Now as for the convertEthToDai
function, it will look like something this:
function convertEthToDai(uint daiAmount, uint deadline) public payable {
address[] memory path = new address[](2);
path[0] = uniswapRouter.WETH();
path[1] = daiToken;
uniswapRouter.swapETHForExactTokens.value(msg.value)(daiAmount, path, address(this), deadline);
// refund leftover ETH to user
msg.sender.call.value(address(this).balance)("");
}
There are several things to unpack here.
- Uniswap Router: The
uniswapRouter
will be a wrapper contract provided by Uniswap that has several safety mechanisms and convenience functions. Currently it is recommended to use the Router02 contract. You can instantiate it usingIUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
) for any main or testnet. The interface code can be found here. - Path: Any swap needs to have a starting and end path. While in Uniswap v2 you can have direct token to token pairs, it is not always guaranteed that such a pair actually exists. But you may still be able to trade them as long as you can find a path, e.g.,
Token1 → Token2 → WETH → Token3
. In that case you can still trade Token1 for Token3, it will only cost a little bit more than a direct swap. - WETH: You might notice that we are using WETH here. In Uniswap v2 there are no more direct ETH pairs, all ETH must be converted to WETH first. In our case this is done by the router.
swapETHForExactTokens
: This function can be used to use ETH and receive and exact amount of tokens for it. Any leftover ETH will be refunded, so make sure you have a fallback function in your contract to receive ETH:receive() payable external {}
. Thedeadline
parameter will ensure that miners cannot withhold a swap and use it at a later, more profitable time. Make sure to pass this UNIX timestamp from your frontend, don't usenow
inside the contract.- Refund: Once the trade is finished, we can return any leftover ETH to the user. This sends out all ETH from the contract, so if your contract might have an ETH balance for other reasons, make sure to change this.
How to use it in the frontend
One issue we have now is when a user calls the pay function and wants to pay in ETH, we don't know how much ETH he needs. We can use the getAmountsIn function to compute exactly that.
function getEstimatedETHforDAI(uint daiAmount) public view returns (uint[] memory) {
address[] memory path = new address[](2);
path[0] = uniswapRouter.WETH();
path[1] = multiDaiKovan;
return uniswapRouter.getAmountsIn(daiAmount, path);
}
Now we can call getEstimatedETHforDAI
in our frontend. To ensure we are sending enough ETH and that the transaction won't get reverted, we can increase the estimated amount of ETH by a little bit:
const requiredEth = (await myContract.getEstimatedETHforDAI(daiAmount))[0];
const sendEth = requiredEth * 1.1;
Fully working example for Remix
Here's a fully working example you can use directly on Remix. It allows you to trade ETH for Multi-collaterized Kovan DAI:
pragma solidity 0.7.1;
import "https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/interfaces/IUniswapV2Router02.sol";
contract UniswapExample {
address internal constant UNISWAP_ROUTER_ADDRESS = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D ;
IUniswapV2Router02 public uniswapRouter;
address private multiDaiKovan = 0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa;
constructor() {
uniswapRouter = IUniswapV2Router02(UNISWAP_ROUTER_ADDRESS);
}
function convertEthToDai(uint daiAmount) public payable {
uint deadline = block.timestamp + 15; // using 'now' for convenience, for mainnet pass deadline from frontend!
uniswapRouter.swapETHForExactTokens{ value: msg.value }(daiAmount, getPathForETHtoDAI(), address(this), deadline);
// refund leftover ETH to user
(bool success,) = msg.sender.call{ value: address(this).balance }("");
require(success, "refund failed");
}
function getEstimatedETHforDAI(uint daiAmount) public view returns (uint[] memory) {
return uniswapRouter.getAmountsIn(daiAmount, getPathForETHtoDAI());
}
function getPathForETHtoDAI() private view returns (address[] memory) {
address[] memory path = new address[](2);
path[0] = uniswapRouter.WETH();
path[1] = multiDaiKovan;
return path;
}
// important to receive ETH
receive() payable external {}
}
Solidity Developer