DeFi projects are taking over the Blockchain world. While many projects are coming forward. Uniswap a DEX protocol caught the eyes of a great number of Blockchain followers. In this publication, we will take a look at what Uniswap protocol is, how it works and we will further take a look at the maths behind the well-known protocol. As of writing this publication, Uniswap has two versions, Uniswap v1 and Uniswap v2. As we move forward we will discuss the different features of Uniswap and how these features in v2 differ from v1.
Uniswap protocol automates the exchange market of tokens. It obviates the need for trusted intermediaries, prioritizing decentralization, security, and censorship resistance. Uniswap is open-source software and is licensed under the GPL.
Uniswap uses the constant product market maker model. This makes Uniswap distinct from other decentralized exchanges. It uses non-upgradable smart contracts to implement this model. People also forked Uniswap and built a whole new protocol on top of the strong code-base. Example: SushiSwap
What Uniswap essentially does is it supports different markets that can be created by anyone and users can trade their tokens in those markets.
Uniswap uses the constant product market maker model. This makes Uniswap distinct from other decentralized exchanges. For Uniwap the pricing is two assets multiplied together will equal to some constant.
For trading token x for y(and vice versa), the market must have reserves Rx > 0 and Ry > 0, and the product formula would then be Rx Ry=k.
The invariant is another word for constant K, and the percentage fee would be 1 − γ. A transaction in this market, trading x > 0 coins x for y > 0 coins y, must satisfy
In Uniswap, anyone can create any pair of two ERC20 tokens. Users can then deposit liquidity, also can swap for the counterpart token.
In v1 every pair had ETH as a base asset. The code for the creation of pair in v1 looks like this:
Here the function is expecting only one parameter, the token address.
If the user wants to trade ABC tokens for XYZ tokens, pairs of ABC/ETH and XYZ/ETH will be affected. The flow of trade was:
The routing in v1 is quite simple as every trade needs to pass through the ERC20/ETH pair route. This reduces the fragmentation of liquidity. As even the pool to which the user is not directly interacting with is participating in providing liquidity to that transaction.
However, exposure to ETH in all pools causes impermanent loss to liquidity providers. Also, traders have to face slippage twice while swapping from ABC/ETH pair to ETH/XYZ pair.
So, in v2 both the assets in pairs are ERC20 tokens. The path of trade that provides the best price possible can be handled at a higher level i.e. either off-chain or an on-chain router or aggregator.
Liquidity providers can create pair contracts for any two ERC20 tokens. When pairs are created the createPair function is triggered.
Here the two ERC20 assets are referred to as tokenA and tokenB, whose addresses enter as the parameter and the address of the pair contract is returned by this function.
Both in Uniswap v1 and Uniswap v2, a single factory contract instantiates the pair contracts. In Uniswap v1, CREATE opcode was used to create these pairs, which meant that the address of such a contract depended on the nonce of the creator.
Uniswap v2 uses Ethereum’s new CREATE2 opcode, which allows us to deterministically generate addresses by passing salt in the parameter.
In Uniswap v1, since every pair includes ETH as one asset, using ETH was slightly more gas-efficient. But, Uniswap v2 supports arbitrary ERC-20 pairs. So, adding such support would double the size of the core codebase, and risks fragmentation of liquidity between ETH and WETH pairs. As a result, native ETH needs to be wrapped into WETH before it can be traded on Uniswap v2. Uniswap v2 implements the IWETH interface for this. Uniswap v2 pair contracts wrap the ETH into WETH, without users knowing. This was also to ensure consistency among all pair standards.
At the time of removing liquidity, if the pair includes ETH, Uniswap v2 gives the option to wrap it into WETH and vice versa.
Price Oracles in Uniswap allows it to calculate the time-weighted average of token pairs at a timestamp. The marginal price at Uniswap, excluding fee, at time t can be calculated using:
Uniswap v1 oracle was prone to price manipulation attack, the attack can change the price right before it is measured. Consider any smart contract that is using a DAI/ETH exchange to settle any kind of transaction. The attacker can easily manipulate the price and then execute the transaction on the other contracts.
In v2 every pair measures the market price before the first trade of each block. If the attacker attempts to manipulate the price, they will have to mine two blocks in a row to carry out a successful attack.
Uniswap v2 stores the end-of-block price to a single cumulative-price variable in the core contract weighted by the amount of time this price existed. This variable represents a sum of the Uniswap price for every second in the entire history of the contract. Meaning that the price at the end of each block is added to the cumulative-price-variable.
This means that the accumulator value, a, at any given time t (after being updated) should be the sum of the spot price p at each second in the history of the contract. Also, the spot price of x in terms of y will be reciprocal of the spot price of y in terms of x.
The time-weighted average price (TWAP) can be calculated by using this cumulative price. TWAP is the price specified over a time period. Users can choose the interval for this period. Although it results in a less up-to-date price, choosing a longer period also makes it more expensive for attackers to manipulate the price.
The core contract updates its reserves after each interaction and updates the price cumulative variable using the price derived from the cached reserves rather than the current reserves. This prevents an attacker from changing the balance and marginal price in the reserves.
An example showing interaction with Uniswap price oracle is here
Solidity does not support non-integer numeric datatype. So, Uniswap v2 uses a binary fixed-point format to encode and manipulate prices. Uniswap created this library, UQ112xUQ112, for this purpose.
Unsigned rational numbers with 112 bits on each side of the decimal point are used to store fixed-point i.e. UQ112.112.
The data type used to store Uniswap reserves is uint112. For calculations, these reserves are then converted into type uint224.
The library makes sure that the reserves don’t overflow but first casting the uint112 reserve into uint224 and then multiplying it with 2**112, which is a huge number and hence won’t overflow.
UQ112.112 format was chosen because, in a 256 bits slot, these reserves can be stored in uint224, leaving 32 bits storage free. Although the price at any given time is sure to fit in uint224, the accumulated result is not. Hence, these 32bits are utilized in price accumulation.
Another point to note is that 32bits are not enough to store a timestamp value that will never overflow. The date when the Unix timestamp overflows a uint32 is 02/07/2106. To ensure the working of this system after this date, and every multiple of 2**32 − 1 seconds thereafter, oracles are simply required to check prices at least once per interval. This is because the core method of accumulation is overflow-safe.
Uniswap v2 only supports reserve balances of up to 2**112 - 1. This number is high enough to support a totalSupply over 1 quadrillion tokens with 18 decimal places. If either reserve balance does go above, any call to the swap function will begin to fail because of a check in the _update function.
To solve this deadlock; the skim() function can be utilized.
Liquidity provider’s assets are stored in the core pair contract. Vulnerability in this contract would cost millions of dollars to be stolen or frozen.
Uniswap v2 contracts are divided into two parts. Core and Periphery. You can visualize it as two layers.
The Core consists of factory and pair contracts, the factory is responsible for creating and indexing the pairs. The protocol charge turning on mechanism is also part of the factory contract. Pair contract keeps track of the pool’s balance and also serves as the automated market maker. Direct interaction with these contracts is not recommended instead, the periphery contracts should be used.
Periphery consists of router contracts. Except for asset swapping all other basic functionalities can be performed in the router contracts.
Users interact with router contracts and router routes the funds of the user to pair contracts. Then the trade is executed upon the calculation of new balances in the pair contract.
Router contracts also have methods of authorization of meta-transactions.
Flash swaps are very similar to flash loans. In flash swaps, users can basically borrow the whole liquidity of a pool and conduct the transaction, requiring that, to pay back the whole amount including swap within that transaction also.
Uniswap v1 didn’t have this kind of feature, but because of contract re-architecture, users can directly interact with the pair contract’s swap method. And users can pass in if they want to flash swap only tokenA or both tokens.
The desired amount that users want to borrow is passed in the first two parameters. And if the users pass in the data parameter, the pair contract will call the Flash swap contract’s method which is basically executing the transaction.
0.3% of swap fees are also charged in flash swaps.
The data parameter verifies whether the swap is a normal swap or flash swap. If the data parameter is empty the swap is expected to be a normal one, otherwise a flash swap.
Anyone can execute flash swap by implementing the IUniswapCallee interface and uniswapv2Call method of this interface.
For a fully functional example of flashswap: here
Uniswap v1 had no protocol fee. In v2 a 0.05% of trade may be received by the protocol if the value of feeTo variable is not zero address. Currently, this value is off, however, a pre-specified address feeToSetter can set the value of feeTo variable, hence turning the fee on, and can also change the value of feeToSetter variable itself.
If the feeTo variable is on, the protocol will take 0.05% out of the 0.3% taken off from the trade which is 1/6th of the total fee amount paid by users.
Sending this 0.05% to the feeTo address will cost an additional gas fee. So, Uniswap accumulates this fee then collects it only which the liquidity changes i.e. liquidity is provided or withdrawn.
To compute the total collected fee, growth in √k is measured, where k is the invariant used in the automated market-making formula. Since √x.y represents the geometric mean, i.e. the increment in terms of ratio.
The above equation gives the accumulated fees at t1 and t2 in terms of percentage. If the fee was turned on before t1, the 1/6th of the accumulated fee should go to the feeTo address.
The contract mints new liquidity tokens and transfers them to the fee receiving address when liquidity is deposited or withdrawn. This amount of tokens to be minted is calculated using the formula
Where sm is shares minted, s1 is the total quantity of outstanding shares at time t1 and is 1/6.
Solving for sm after substituting f1,2 and ϕ we get
In Uniswap v2, users can authorize a transfer of their pool shares with a signature. Anyone can submit this signature on the authorizer’s behalf by calling the permit function, paying gas fees, and possibly performing other actions in the same transaction.
Uniswap v2 removes the approval transaction to remove liquidity from their app. Hence the UX is improved and users' gas fee is also saved.
Uniswap v1 uses Vyper language while v2 is written using solidity. At the time when v2 was being developed, Vyper didn’t interpret the non-standard ERC20 tokens, nor did it allow access to opcodes using inline assembly. There may be other limitations as well. Hence, Solidity replaced Vyper in Uniswap v2.
The ERC-20 standard requires that transfer() and transferFrom() return boolean. Tokens like Tether(USDT) and Binance(BNB) have no return value.
Uniswap v1 interprets the missing return value of these improperly defined functions as false, causing the attempted transfer to fail.
Uniswap v2 handles non-standard implementations differently. Specifically, if a transfer() call has no return value, Uniswap v2 returns true.
Uniswap uses the solidity call() method to execute the ERC20 contract’s methods. The call() method returns data and bool variables.
The above code is an example of safeTransfer function.
Uniswap v1 also makes the assumption that calls to transfer() and transferFrom() cannot trigger a reentrant call to the Uniswap pair contract. This assumption is violated by certain ERC-20 tokens, including ones that support the hooks of ERC-777. To provide support for such tokens, Uniswap v2 includes a lock that directly prevents reentrancy.
In Uniswap both v1 the trading fee i.e. 0.3%, is cut from the return/output amount.
The contract implicitly uses the formula
(x1- 0.003 xin) . y1 >= x0 . y0
Where x0 . y0 and x1. y1 are balances of tokenA and tokenB respectively. And xin is the input amount that the user intends to swap.
In v2, because of flash swaps, users may use both the assets for swapping and so the formula becomes,
(x1- 0.003 xin) . (y1- 0.003 yin) >= x0 . y0
When liquidity is added in Uniswap v2 the reserves are updated. However, in a case where tokens are deposited in contract, the balance of the contract would vary from the amount of reserves. The trade performed while balances and reserves are not synced would have incorrect values. The function sync() sets the reserves of the contract according to the balances.
If the user deposits balance in the contract and this balance exceeds uint112. Calling the sync() function is not suitable. Since the _update() function in sync() uses uint112 which will overflow. In such a case the function skim() is to be used. Skim will remove the access balance and resolve the deadlock of clogging the uint112.
When liquidity is deposited in a pair, liquidity tokens are minted. The amount of how many liquidity tokens are to be minted is calculated using the formula:sminted=xdepositedxstartingsstarting
But when the first liquidity is deposited i.e. xstarting=0. Then the formula would not work. In v1 initial share supply of tokens is equivalent to the amount of ETH deposited. In v2, the initial share supply is calculated by the formula sminted=xdeposited . ydeposited. This formula makes sure that the liquidity shares at any time depending upon the ratio at which liquidity was initially deposited. Not on the amount of base token on that pair.
Uniswap v2 burns the first 0.000000000000001 i.e. (1**-15) minted pool shares, which is a negligible amount for most tokens. As it is 1000 times the minimum quantity of pool shares. So, once a pair is created and suppose a liquidity provider removes all liquidity resulting in the pool to have 0 reserves and then deposits liquidity again. The initial liquidity is already set and won’t be initialized again. Furthermore, burning the 1**-15 pool shares initially also prevents a situation in which the worth of the minimum quantity of liquidity pool share i.e. 1**-18, becomes high. High enough that the small liquidity providers will be unable to provide liquidity.
This process of burning takes place in the function mint(), where minimum liquidity is minted to send to the zero address.
Xord is a Blockchain development company providing Blockchain solutions to your business processes. Connect with us for your projects and free Blockchain consultation.