Interfacing With UniswapV2Router02: 3 ― Talking to UniswapV2Router02

Now that the information about tradable token pairs, their contract addresses, and the addresses of the tokens that make up each pair has been loaded, the app can call functions on UniswapV2Router02 to execute swaps. Before the actual swap can happen, there are a few prerequisites that need to be taken care of:

Furthermore, the swap can only succeed if the user's balance of input token is sufficient. Their current balance can be queried from the token contract. This is again part of the ERC20 standard.

There's another and more fundamental prerequisite though which hasn't been adressed. How is the app going to talk to these contracts (router, pairs, tokens) in the first place? In order to do so, it must ultimately be able to send messages to an Ethereum node's JSON-RPC interface. In a web browser context, this is typically left to an extension that handles communication with JSON-RPC endpoints, and on top of that keeps a list of the user's accounts ('wallets'). The DApp will use the JavaScript interface provided by this extension to send messages such as eth_call and eth_sendTransaction to the Ethereum node, which, on a higher layer, constitute 'talking' to contracts on the blockchain. Probably the most widely used of these extensions is MetaMask. Some popular mobile wallets also have a web browser view with an injected Ethereum provider.

When developing a React DApp, web3-react lends itself extremely well to being the link between React components and the Ethereum provider. Apart from being able to use injected (JavaScript) providers, web3-react offers a number of other connectors, some of which meant to be used with hardware wallets, others with hosted wallet solutions (Portis, Torus, …) which have also become popular. My token swap uses @web3-react/injected-connector only.

Output amount calculation

The output amount calculation is based on the reserves currently in the pair and takes into account the 0.3% trading fee charged by Uniswap:

vout=997vinRout997vin+1000Rinv_{out} = \frac{997\cdot v_{in}\cdot R_{out}}{997\cdot v_{in}+1000\cdot R_{in}}

where Rin, RoutR_{in},\ R_{out} are the input and output reserves of the pair, and vinv_{in} is the input amount set by the user. Normally, this calculation would be left to Uniswap's JavaScript SDK [pair.ts::getOutputAmount], but since the point of this project was to reconstruct parts of Uniswap, I've incorporated it in my app's token swap API. The current reserves are on-chain data fetched directly from the pair contract. Since TypeChain is used to generate TypeScript bindings from Uniswap artifacts as described in part one, those are imported:

import {
IERC20__factory,
IUniswapV2Pair__factory,
IUniswapV2Router02__factory,
} from '../../typechain'

…and then used to comfortably (i.e. with type checking and autocomplete) connect with an instance of UniswapV2Pair and call its getReserves function:

    // Call pair contract to fetch current reserves.
const pairInstance = IUniswapV2Pair__factory.connect(pair.id, this.provider)
const {reserve0, reserve1} = await pairInstance.getReserves()

Approving UniswapV2Router02 for spending user's input token

The ERC20 token standard specifies a transferFrom function which is ultimately used to transfer input token from the user to a liquidity pool, and output token from a pool to the user. Because it is the Uniswap router that will be calling transferFrom, it needs to be approved for spending tokens on behalf of the user first. To this end, approve (also part of the ERC20 standard) is called with the address of UniswapV2Router02 as spender:

  approve: (token: Token) => Promise<TransactionResponse> = (token) => {
const signer = this.provider.getSigner()
const inputTokenInstance = IERC20__factory.connect(token.id, signer)
return inputTokenInstance.approve(UNISWAP_V2_ROUTER02, ethers.constants.MaxUint256)
}

Note how we approve the router for an amount of MaxUint256 (225612^{256}-1): this effectively gives the router permission to spend this token on behalf of the user 'forever', until revoked by calling approve again with an amount of zero. There is discussion on security concerns around virtually unlimited allowances, but that's out-of-scope here.

The current allowance for a spender address can be queried by calling allowance on the token contract. That's what the token swap component periodically does to determine whether the swap button is in Approve <TOKEN_SYMBOL> or in Swap state. The app uses the SWR package for this kind of polling. With it, the React component can keep track of the current allowance using the useSWR hook:

  // Poll for token approval.
const {data: allowance} = useSWR([account, chainId, inputAmount, token0?.id], fetchAllowance, {
refreshInterval: 7000,
isPaused,
})

The fetchAllowance callback calls a token swap API function to fetch the current allowance from the token contract. The array passed to useSWR can be thought of as dependencies, similar to what might be passed as dependencies to a useEffect hook. For instance, in this example, passing account as a dependency causes the allowance to be re-fetched instantly when account changes resulting from the user selecting a different account in MetaMask. The same hook is used to fetch other on-chain data as needed: the user's balance of the input token, and the current pair's reserves (for calculating the output amount).

Executing the swap

Finally, when the projected output amount is known, and it has been verified that the user's input balance is sufficient, and that they have approved UniswapV2Router02 for spending their input token, the actual swap can be executed by calling one of the router's swap functions. Since my app only ever issues swaps where an exact amount of input token is given, and the output amount is inexact, only three of the router's swap functions need to be taken into consideration:

Whenever a swap involves ether on one end, the router will be using a pair where one of the tokens is WETH (Wrapped Ether): a pair always consists of two ERC20 tokens, hence the need to wrap ether into an ERC20 token. The transaction sent from the user's Ethereum provider when they buy tokens in exchange for ether has an ether payload in the value field, as does the final transaction that pays out the user when they sell tokens for ether. When swapping two tokens that aren't ether, on the other hand, the only ether spent on the transaction is for gas, whereas the exchange of value is realized solely through the transferFrom function of the tokens involved.