UniswapV2 mathematical approach and contract code review

✅ Follow me: X: @Younsle1 Warpcast: @zer0luck

Goal

I will learn about simple price rules, marginal price, and price impact through reviewing a whitepaper and exploring new features such as flashswap, MetaTX, and EIP712, while also studying the EVM non-integer and UQ112.122. I will familiarize myself with the protocol fee, sync, and skim functions, as well as the Uniswap Contract V2-core and v2-periphery, and delve into related mathematical theorems.

UniswapV2 Simple Price Rule

  • AMM-based CPMM (Constant Product Market Maker) Model

  • Has two token pairs.

  • Pi = 0.3% ( = 30 bp) = ø = 0.997 = 99.7% (fee)

  • In the case of Uniswap V2, any token pair is treated the same as ETH/DAI. Arbitrator continues to maintain.

    • In the case of Uniswap V2, since it is impossible to maintain only liquidity providers and traders, the Arbitrator must continue to operate.

    • Because traders unbalance until they act

Simple Price Rule mathematical theorem
Simple Price Rule mathematical theorem
  • The calculation formula for when I want to have as much as delta x, that is, the output is fixed

  • Here, there are reserve x, y, but you can check that you subtract because you take as much as delta x. And in the case of delta y, Uniswap measures fees for inputs, so you can see that ø is multiplied.

  • As a result, k = x * y (input: delta y, output: delta x, this CPMM Model )

Marginal Price & Price Impact

Marginal Price & Price Impact
Marginal Price & Price Impact
  • In the case of marginal price, sending delta x (amount) to 0 means the slope of a certain tangent line.
Marginal Price & Price Impact mathematical theorem
Marginal Price & Price Impact mathematical theorem
  • Marginal Price can be obtained with the above formula, and can be implemented as an intermediate formula in the process of delta x converging to 0.

  • If ø is 1, the last expression can be expressed. In other words, it can be seen that the marginal price is the absolute value of the difference between the two tokens. (It can be seen as the same as the tangent line.)

UniswapV2 White Paper Review

UniswapV2 New Features

  • ERC20/ERC20 Pairs (WETH, A/B as ETH was mandatory bridge)

    • In the case of V1, a pair is created between Ether and another token, so when swapping between tokens A and B, there is a process of two exchanges, resulting in a lot of fees. So, to solve this problem, V2 created an ERC20/ERC20 pair.

    Coin VS Token

    Coin: If you pay the gas fee, which is the governance of the mainnet like Ethereum, and the reward that Validator receives, it is a coin. The token that got its name from the mainnet is a coin (BSC, is a coin)

    Token: The rest of the coins on the mainnet are tokens. (Ethereum is a token)

    • Now, in order to make Ethereum a little easier, WETH is developed, and when you deposit Ethereum, the same amount of Ethereum tokens are issued in a 1:1 correspondence
  • Robust price oracle (cumulative price + time weighted average, track both prices, divide current & accounted reserves)

    • Have cumulative price, use time weighted average and record token price for each

    • Current, canceled reserves were used.

Robust Price Oracle

Robust Price Oracle
Robust Price Oracle
  • Each r represents token a and b reservers. Here, how much token A is per token B, and the price is one.
cumulative sum
cumulative sum
  • Uniswap V2 accumulates this price, by keeping track of the cumulative sum
price oracle
price oracle
  • Record 2 of each. If you do cumulative price here, the price will continue to be added every second.

    • However, since transactions continue to accumulate on the blockchain, it is updated only when an event (swap) occurs, so based on block.timestamp, compare the most recent time with the most recently entered time to know the interval, add the previous ones, and create a new one. to the price. (time-weighted average)

    • Since it is time-weighted, it is divided by time. (t2 - t1), just subtract the one in between. As a result, it provides a price oracle.

Price Oracle Issue (solve Uniswap V3)
Price Oracle Issue (solve Uniswap V3)
  • However, a problem occurs on the Price Oracle. Price Token a's t1 and t2 do not match the denominator and numerator 1 of Price Token B's t1 and t2. Uniswap V3 found another way to solve this problem.

Flash Swap

  • External contract can be used. In the case of DEX and DEFI, composability is higher than that of CEX, so there may be lending following the back end, and various things such as flash loans can be used within one transaction. In swapping this using a callback function, tokens can be exchanged before transmission or can be transmitted as desired by the user. It is used later to fit the invariant.

  • So, if the user does not have money, borrow it and make it possible to swap in Uniswap. This is possible because it is an atomic transaction (transactions cannot be divided). If even one of the require statements does not pass, all of the previous transactions cannot be established.

  • Originally, people who traded at CEX come and see Deif, they are surprised to see that MDD converges to 0… There is a problem if anyone can follow it and only draws capital and calculates, so if anyone does the wrong thing with money.

  • Regarding defi, I think that there must be a defense against admiralty rights so that it can be opened to people. (To avoid situations like anchors)

Meta Transaction

  • As for the meta transaction, from EIP-712, in the case of the original ERC20, Approve was made separately, that is, two transactions! After I Approve and the Allowance increases, Swap is possible

  • By combining these operating processes into one, permit is created and meta Tx is developed to support this.

Ethereum:

STAKE, wNXM, BAL, ALEPH, AMPL, renBTC, cETH, DAI, renZEC, renBCH, YAM, JRT, PRQ, HAKKA, UNI, AAVE, ZEE, ANT, HEZ, AUDIO, UFT, aAAVE, aBAT, aBUSD, aDAI, aENJ, aKNC, aLINK, aMANA, aMKR, aSNX, aSUSD, aTUSD, aUNI, aUSDC, aUSDT, aWBTC, aWETH, aYFI, aZRX, TORN, 1INCH, PPAY, COVER, USDFL, pBTC35A, NDX, SNOW, OPIUM, renDOGE, BMI, LON, ARCH, $TRDL, BT, VSP, RAI, PREMIA, POOL, NORD, RAD, MATTER, RULER, INV, DFX, YIELD, ODDZ, VTX, TRIBE, FEI, LUSD, MIST, PUSH, BOSON, FORTH, SWISE, ICE, MARSH, GLM, UDT, ROUTE, CTX, BANK_BANKLESS, GTC, VUSD, DEFT, DFYN, LQTY, INST, ARCX, DVF, EDEN, TMM, GEL, wCFG, wstETH, MC, ENS, PSP, RBX, GAS, TEMP, T, OOKI, agEUR, ANGLE, SIS, BBS, SYN, THOR, OHM, COW, CULT, FNC, NFP, gOHM, DATA, TIC, VISION, FOXy, ELK, MYST, HONEY, HOP, PDI

Newly developed with no first-class type support for EVM non-integer? (precision)

  • EVM is crap! Floating point, decimal representation Irrational number representation No calculations Poor optimization

  • Since the first-class type is not supported for non-integer, it is newly created. How?

UQ112.112
  • 112, 112 divided like this. The range is 0 , 2^112 - 1, but the precision is 1 / 2^112, so the first 112 of UQ112.112 is calculated within the range as follows.
UQ112.112
UQ112.112
  • Since the precision is 1/2^112, multiplying by 1 is the same, and multiplying by 0 to find the minimum value. Finally, if you want to find the maximum value, multiply it by 2^0. At this time, if it is 2^112, the front is 1, but since minus 1, it is 111111... is assigned

  • 112 at the end is basically unsigned and is an expression.

But why 112?

  • Since there is an unsigned type using uint224, 112 bits represent the previous significant digit, and the remaining 112 is the exponent. 112 + 112 = 224, and usually unsigned is 256, so 32 bits if subtracted from each other!

  • Expresses 32 bits as time.

  • The remaining 32 bits are used as time, which is used in UTC notation. Since people have been around for a long time, cast to uint32(time) form for UTC-based time and truncate it. Only look at the last one and use the interval difference using under/over flow in between.

  • 2^32 - 1 second is very long. So, if 2^10 is 1000 and (10^3)^3, it takes almost 110 years.

  • Since there is unsigned 32bit, since the Robust Price Oracle Price is a rational expression, UQ112.112 is used.

cumulative sum
cumulative sum
  • And it can be seen as 32 bits for the previous time.

Protocol Fee(feeTo is Set!!)

  • The protocol fee must be received for the accumulated fee, and since this protocol operator must also receive money, all swap fees go to the liquidity provider unless the protocol fee is turned on.

  • In addition, if the protocol fee is turned on, accumulated fee will occur. Only when liquidity becomes deposit/withdraw. This is to prevent this because the gas fee increases if you continue to give it to the protocol.

  • This method is as follows.

Protocol Fee
Protocol Fee
  • When the root K2 is at t2, the root K1 is at the time of t1 (is time). In the end, the fee is collected only when f is positive. The fee is collected when K2 is higher, i.e. when add liquidity.

  • Here, I said '0.005%' fee, but earlier, it was for collection as much as '0.3%', but it is '1/6' at the rate of '0.05/0.3'. s1 is the original LP token. If you deposit, the minting amount Sm / S1 + Sm = ⌀ f 1,2 is 1/6 of the total. f 1, 2 will be 5/6, meaning we will give 1/6 to the protocol

  • To further organize minting, it is the case of assigning 1/6 to , and sm goes as minting fee.

Basic version for important CPMM

  • Adjustment for fee (x & y k increase!, reserves ≠ balances)

  • If x, y is input from outside, x, y or k can be increased.

  • The important point is that k is calculated as an invariant, but actually k increases. ?

    • Reserves are not directly distributed to liquidity providers, but basically, the reward structure increases the LP price itself. why? as k grows

    • If Ethereum is 1000 and USDC 10 pairs, the swap volume itself is too high. If the amount is 10 and 1000 respectively, what if it increases to 20 and 2000? The number of LPs is the same, but the value of LPs increases.

  • Reserves and balances are different!! (reserve ≠ balances)

    • reserves: actual calculation

    • balances: number of tokens

  • In the case of UniswapV1, there is a trading fee, 0.3%, and 'constant-product', so enforcing is proceeded. So input is everything because there is no such thing as flashSwap. Here x1 is different from x0. When I actually calculate, the balances are updated

  • If I put input, balances increase

  • If it has risen, proceed with enforcing (y must be left out)

  • The amount I actually put in should be subtracted by 0.003% (to go to the liquidity provider), that value is more than the K value

non flashswap in CPMM
non flashswap in CPMM
flashswap in CPMM
flashswap in CPMM
  • Because UniswapV2 is a flashswap, it is possible on both sides. However, we did not put in values multiplied by 0.3%, but multiplied by 1000 for each. Why? This is because it is composed of unsigned int. If I multiply by 0.003, making it big ah because the lower decimal point can disappear! However, for each reserve, it is uint112, so it is multiplied by about 1000.
CPMM based on unsigned optimization
CPMM based on unsigned optimization

Sync(), Skim() What are these functions?

  • sync() and skim() → reserves ≠ balances, tax token

  • tax token?

    • When transferring, x (input) decreases. When swapping correctly, there is a swap fee that I originally go through and I have to pay an additional tax token.
  • So, in the above formula (x1 - 0.003*xin)*(y1-0.003*yin) >= x0*y0, there are often cases where k should not be greater than x0*y0. To deal with this specific case in a general way, uniswap V2 was developed.

  • In the case of sync(), there is also a tax token, that is, the balances themselves listen to deflate. used as a recovery mechanism.

  • There can be a case where the totalsupply exceeds 2**112, and the liquidity provider stops when this pair fills up to 2**112 - 1, so it plays a relaxing role.

  • In the case of skim(), it is about overflow when going to pair, and uint112 storage is used, but it is filled up to prevent overflow (2**112-1 until overflow, until current balances)

UniswapV2 Contract Code Review

UniswapV2 : v2-core

https://github.com/Uniswap/v2-core/tree/master/contracts
https://github.com/Uniswap/v2-core/tree/master/contracts

UniswapV2ERC20.sol

  • Used for defining LP token.

  • ERC20 wrap to EIP712 for using permit function

UniswapV2Factory.sol

  • Manage pairs (creation, number, protocol fee on/off setting)

  • Manage pools(UniswapV2Pair contracts) create Pair, get pari number, set fee to, set feeTo setter

    • set fee to: The address is default 0 through the protocol set fee to, but if it is not 0, the fee can be collected

    • set feeTo setter: The setter must be changed to several names to match the decentralized nature of the blockchain. (To charge on a protocol, you must decide through voting.)

UniswapV2Pair.sol

  • Most of UniswapV2’s Core functions are adapted

UniswapV2 : V2-periphery

https://github.com/Uniswap/v2-periphery/tree/master/contracts
https://github.com/Uniswap/v2-periphery/tree/master/contracts

UniswapV2Migrator.sol

  • Migrate liquidity from UniswapV1

  • In order to transfer all the liquidity that was in Uniswap V1, they automatically withdrew and deposited in uniswap v2. But there has been a lot of Rug-pull?

    • My LP, because I handed over my authority, my entire property was hit by users whose credit was not guaranteed. In the end, malicious users tend to steal money and then launder it.
  • Migration itself makes it easy to transfer liquidity, and it is difficult to change on the blockchain because it is difficult to transfer existing customers.

UniswapV2Router02.sol

  • add/remove liquidity, swap, swapSupportingFeeOntransferTokens, quote, getAmountsOut, getAmountsIn

v2-core/UniswapV2Pair

pragma solidity =0.5.16;

import './interfaces/IUniswapV2Pair.sol';
import './UniswapV2ERC20.sol';
import './libraries/Math.sol';
import './libraries/UQ112x112.sol';
import './interfaces/IERC20.sol';
import './interfaces/IUniswapV2Factory.sol';
import './interfaces/IUniswapV2Callee.sol';

contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
    using SafeMath  for uint;
    using UQ112x112 for uint224;

    uint public constant MINIMUM_LIQUIDITY = 10**3;
    bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));

    address public factory;
    address public token0;
    address public token1;

    uint112 private reserve0;           // uses single storage slot, accessible via getReserves
    uint112 private reserve1;           // uses single storage slot, accessible via getReserves
    uint32  private blockTimestampLast; // uses single storage slot, accessible via getReserves

    uint public price0CumulativeLast;
    uint public price1CumulativeLast;
    uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event

    uint private unlocked = 1;
  • Pair is actually an LP Token, inherited from UniswapV2ERC20 to use permit.

  • UQ112x112 floating point expression

Why use UniswapV2 Pari Mimum liquidty

  • MINIMUM_LIQUIDITY is 10**3. It may be possible to prevent indiscriminate proliferation of pairs, but in fact, it burns. Since 1000 burns, it means that `MINIMUM_LIQUIDITY' must be exceeded.

  • UniswapV2 mints Liquidity Shares equal to the geometric average of the initially deposited amount. The value of liquidity pool shares can increase over time, either by accumulating transaction fees or by “donating” to the liquidity pool. Theoretically, this could lead to a situation where the value of the minimum number of shares in the liquidity pool (1e-18 pool shares) is so great that it is impossible for small liquidity providers to provide liquidity.

  • To mitigate this, UniswapV2 incinerates the initially issued 1e-15 pool shares (1000times the minimum quantity of pool shares) and sends them to address (0), not the issuer. This is a negligible cost for any token pair. However, this greatly increases the cost of the above attack, so an attacker would actually have to contribute $100,000 to the pool to raise the value of their stake in the liquidity pool to $100. (Liquidity is permanently locked up)

  • 10**3 given in the code is equivalent to 1e-15 of a single LP Token. This value is to increase the attack cost by orders of magnitude without causing too much damage to the first liquidity provider.

  • factory = msg. sender

  • token0, token1 = ERC20

  • reserve0 (amount token 0 used when calculated internally), I entered it now, but it is not established when I put the expression into the calculation right away. why? Because the balances are already increased, if they increase, they must be reduced, so the balances are increasing because they are initially fixed in reserve.

  • blockTimestampLast = uint32(block.timestamp) UTC time

  • price0CumulativeLast = time weighted price is calculated to update when swapping because there is no need to update even though the price has not changed

  • kLast = reserve0 * reserve1

  • unlocked = 1 , whether to run migration

constructor() public {
    factory = msg.sender;
}

// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
    require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
    token0 = _token0;
    token1 = _token1;
}

// update reserves and, on the first call per block, price accumulators
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
    require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
    uint32 blockTimestamp = uint32(block.timestamp % 2**32);
    uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
    if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
        // * never overflows, and + overflow is desired
        price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
        price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
    }
    reserve0 = uint112(balance0);
    reserve1 = uint112(balance1);
    blockTimestampLast = blockTimestamp;
    emit Sync(reserve0, reserve1);
}
  • After going through the process of calculating the maximum allocation size through uint112 within the _update function, proceed with typecasting to blockTimestamp 32 bits.

  • uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired See the interval between the difference between the current blocktimestamp and the last blocktimestamp. (Overflows may occur, but for purposes of measuring prices, it doesn't matter.)

  • timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0 The reason why timeElapsed must be greater than 0 is 0 because it is the same time zone within a block if it can be called multiple times. However, if you come out of another block, it will be 30 seconds.

  • Using UQ112x112, perform the following process to find the priceCumulativeLast value.

price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
priceCumulativeLast
priceCumulativeLast
  • Divide _reserve1 by _reserve0, conversely, divide _reserve0 by _reserve1, which means price. After that, you can check the above formula through the process of continuously adding values to the priceNCumulativeLast state variable, and you can confirm that it is time-weighted because it is multiplied by the previously calculated timeElapsed value.
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
  • The update is completed by allocating uint112 (balance n) as reserve n, and event processing proceeds after updating the value of blockTimestamp in the same way.
// if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
    address feeTo = IUniswapV2Factory(factory).feeTo();
    feeOn = feeTo != address(0);
    uint _kLast = kLast; // gas savings
    if (feeOn) {
        if (_kLast != 0) {
            uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
            uint rootKLast = Math.sqrt(_kLast);
            if (rootK > rootKLast) {
                uint numerator = totalSupply.mul(rootK.sub(rootKLast));
                uint denominator = rootK.mul(5).add(rootKLast);
                uint liquidity = numerator / denominator;
                if (liquidity > 0) _mint(feeTo, liquidity);
            }
        }
    } else if (_kLast != 0) {
        kLast = 0;
    }
}

//...

// force balances to match reserves
    function skim(address to) external lock {
        address _token0 = token0; // gas savings
        address _token1 = token1; // gas savings
        _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
        _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
    }

    // force reserves to match balances
    function sync() external lock {
        _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
    }
  • In the _mintFee function, feeTo retrieves data from the factory and checks whether it is address(0). (Already set state) It may have been created for the first time, so it needs to be checked (because one of x and y is 0)

  • _kLast is k1 (the target to be updated),

  • uint numerator = totalSupply.mul(rootK.sub(rootKLast)); To obtain the sm value, proceed through the root k2 - k1 in the numerator and multiply by totalSupply (S1). As a result, it can be confirmed that liquidity (sm) is obtained, and minting is not issued if it is less than 0.

Protocol Fee
Protocol Fee
  • skim() forces balances, why? Because of the taxToken. Since the balances and reserves in the address are different (exclude balances), only values up to 2**112 are processed to prevent overflow for the range up to the maximum value of 2**112 -1.

  • sync() forcibly adjusts reserves, allocating 2**112-1 in a way that fills in all values.

// this low-level function should be called from a contract which performs important safety checks
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
    require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

    uint balance0;
    uint balance1;
    { // scope for _token{0,1}, avoids stack too deep errors
	    address _token0 = token0;
	    address _token1 = token1;
	    require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
	    if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
	    if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
	    if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
	    balance0 = IERC20(_token0).balanceOf(address(this));
	    balance1 = IERC20(_token1).balanceOf(address(this));
	    }
	    uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
	    uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
	    require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
	    { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
	    uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
	    uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
	    require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
    }

    _update(balance0, balance1, _reserve0, _reserve1);
    emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
  • If you look at the most important swap system parameter, you can see that “OUT” is all included. Why is that? This is because data can implicitly have a new “INPUT”.

  • Since external contracts can be called, set them all to “OUT”.

  • You can see the logic written in the block inside the scope ({ … }) within the code, but why is that?

    • Since the EVM is very terrible, it is not optimized and the limit on the stack is small, so to solve it, create an internal scope, resolve local variables from the inside, and then blow again and proceed with rewriting.

    • In order to solve these problems, it is better to use structs to handle them.

if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
  • Process the sending task because there could be "OUTPUT" data from previously incoming data.
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
  • The statement appears to be discussing a code implementation related to a flashswap feature, which is used through the uniswapV2Call function.

  • The code then retrieves the current balance and verifies it through the balance > reserve - amountOut validation. This operation assigns a value to amountIn. If the calculation results in a balance greater than _reserve - amountOut, the value balance - (_reserve - amountOut) is assigned. However, if the balance falls below, it returns 0.

CPMM based on unsigned optimization
CPMM based on unsigned optimization
//...
  uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
  uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
  require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}

_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
  • Finally, the code implements the flashswap and performs the calculation as described above. The require statement is used to determine if the calculation satisfies the Constant Product Market Maker (CPMM) criteria.

UniswapV2 : v2-periphery/UniswapV2ruter02, UniswapV2Library

// **** SWAP ****
// requires the initial amount to have already been sent to the first pair
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
  for (uint i; i < path.length - 1; i++) {
      (address input, address output) = (path[i], path[i + 1]);
      (address token0,) = UniswapV2Library.sortTokens(input, output);
      uint amountOut = amounts[i + 1];
      (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
      address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
      IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
          amount0Out, amount1Out, to, new bytes(0)
      );
  }
}
  • The _swap function that calculates internally sorts the input and output A, B tokens in ascending order based on their representation in 16-hexadecimal when they are swapped.

  • This is because the tokens can be entered as A/B and B/A respectively. For example, in the case of the CREATE2 function, the token address is determined deterministically.

  • LP tokens can be found all at once, because they are already sorted, so we can find LP tokens, determine what tokens are, and then refer to the values in the format of amounts[i + 1] while incrementing one by one.

(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
  • If input and token0 are equal, assign (uint(0), amountOut), otherwise assign (amountOut, uint(0)).

  • Find each pair through the pairFor function. This retrieves the pair pool address of the output and path[i+2] token and the next token, and if it grows further, it will reference _to.

IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
    amount0Out, amount1Out, to, new bytes(0)
);
  • Before executing the swap, find the pair for input and output and then perform the swap. The new bytes(0) parameter means not to do a flashswap, which is usually executed when performing a canonical swap.
// returns sorted token addresses, used to handle return values from pairs sorted in this order
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
  require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
  (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
  require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
}

// calculates the CREATE2 address for a pair without making any external calls
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
  (address token0, address token1) = sortTokens(tokenA, tokenB);
  pair = address(uint(keccak256(abi.encodePacked(
          hex'ff',
          factory,
          keccak256(abi.encodePacked(token0, token1)),
          hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
      ))));
}
  • The sortTokens function is used to compare two tokens A and B, and if the result is 0, it means that the matching is not found and an error will occur. Only token 0 is checked because the tokens must be different from each other, so as long as either A or B is not 0, the function will work.

UniswapV2 : v2-periphery/UniswapV2Library:getAmountOut, getAmountIn

// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
    require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
    require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
    uint amountInWithFee = amountIn.mul(997);
    uint numerator = amountInWithFee.mul(reserveOut);
    uint denominator = reserveIn.mul(1000).add(amountInWithFee);
    amountOut = numerator / denominator;
}

// given an output amount of an asset and pair reserves, returns a required input amount of the other asset
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
    require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
    require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
    uint numerator = reserveIn.mul(amountOut).mul(1000);
    uint denominator = reserveOut.sub(amountOut).mul(997);
    amountIn = (numerator / denominator).add(1);
}
getAmountOut
getAmountOut
  • "getAmountOut" function calculates the amount of output token in exchange for a given amount of input token. The formula is (reserveOut * (997 * amountIn) / (1000 * reserveIn) + (997 * amountIn)). The value of "amountIn" is the input token amount and "reserveIn" and "reserveOut" are the reserves of input and output tokens in the liquidity pool, respectively. The constant value of "997/1000" is used to avoid an unsigned integer overflow.
getAmountIn
getAmountIn
  • The getAmountIn function calculates the amount of token x that can be bought for token y. x is "reserve out" and y is "reserve in", and delta x is "amount output".

  • The final calculation is (1/997 * ((1000*reserveIn * amountOut) / (reserveOut-amountOut))) + 1, which is done to avoid unsigned integer overflow when computing (1000*reserveIn * amountOut) / (reserveOut-amountOut). The addition of 1 is done because if the result of (numerator / denominator) is 0 due to unsigned integer overflow, it means that the output is non-zero but the input would still be 0, which would allow the pair to be continuously subtracted from the pool.

UniswapV2 : v2-periphery/UniswapV2Library:getAmountsOut, getAmountsIn

// performs chained getAmountOut calculations on any number of pairs
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
    require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
    amounts = new uint[](path.length);
    amounts[0] = amountIn;
    for (uint i; i < path.length - 1; i++) {
        (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
        amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
    }
}

// performs chained getAmountIn calculations on any number of pairs
function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
    require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
    amounts = new uint[](path.length);
    amounts[amounts.length - 1] = amountOut;
    for (uint i = path.length - 1; i > 0; i--) {
        (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
        amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
    }
}
  • The canonical form of calculating swap (out → for given input / in → for given output)

  • Get the pair data through getReserves, then perform the calculation in canonical form (i.e., it is calculated linearly).

UniswapV2 : v2-periphery/UniswapV2Router02:swapExactTokensForToken, swapExactETHForTokens

function swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
    amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
    require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, to);
}
function swapTokensForExactTokens(
    uint amountOut,
    uint amountInMax,
    address[] calldata path,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
    amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
    require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, to);
}
  • Exact means "give", Tokens means ERC20 tokens, ETH means the coin 2 * 3 = 6 function

  • Exact can go before or after:

    • Before: "input amount" is fixed (I will sell this much)

    • After: "input amount" is fixed (I will buy this much)

  • The swapExactTokensForTokens function is ERC20/ERC20 and the "input amount" is fixed.

  • The swapTokensForEaxtTokens function is ERC20/ERC20 and the "output amount" is fixed.

function swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
    amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
    require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, to);
}
function swapTokensForExactTokens(
    uint amountOut,
    uint amountInMax,
    address[] calldata path,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
    amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
    require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, to);
}
  • In the swapExactETHForTokens function, when ETH comes in, you need to check its value, but in the code below, there is no "amountIn", only "msg.value", which is used. In other words, msg.value is amountIn. However, using ETH directly is difficult, so you deposit it as WETH.

  • In swapTokensForExactETH, on the other hand, since ETH is going out, you can simply withdraw it as WETH.

function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
    external
    virtual
    override
    payable
    ensure(deadline)
    returns (uint[] memory amounts)
{
    require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
    amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
    require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
    IWETH(WETH).deposit{value: amounts[0]}();
    assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
    _swap(amounts, path, to);
}
function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
    external
    virtual
    override
    ensure(deadline)
    returns (uint[] memory amounts)
{
    require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
    amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
    require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, address(this));
    IWETH(WETH).withdraw(amounts[amounts.length - 1]);
    TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
    external
    virtual
    override
    ensure(deadline)
    returns (uint[] memory amounts)
{
    require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
    amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
    require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, address(this));
    IWETH(WETH).withdraw(amounts[amounts.length - 1]);
    TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
    external
    virtual
    override
    payable
    ensure(deadline)
    returns (uint[] memory amounts)
{
    require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
    amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
    require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
    IWETH(WETH).deposit{value: amounts[0]}();
    assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
    _swap(amounts, path, to);
    // refund dust eth, if any
    if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
}

UniswapV2 : v2-periphery/UniswapV2Router02:_swapSupportingFeeOnTransferTokens

// **** SWAP (supporting fee-on-transfer tokens) ****
// requires the initial amount to have already been sent to the first pair
function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
    for (uint i; i < path.length - 1; i++) {
        (address input, address output) = (path[i], path[i + 1]);
        (address token0,) = UniswapV2Library.sortTokens(input, output);
        IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
        uint amountInput;
        uint amountOutput;
        { // scope to avoid stack too deep errors
        (uint reserve0, uint reserve1,) = pair.getReserves();
        (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
        amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
        amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
        }
        (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
        address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
        pair.swap(amount0Out, amount1Out, to, new bytes(0));
    }
}
  • The _swapSupportingFeeOnTransferTokens function transfers 10% of the tax token to the protocol each time a swap is made because of the tax token. The value of k is not maintained and decreases immediately after transfer.

  • The amountInput is calculated by subtracting the reserveInput from the balance of the input token in the pair contract. The input token can be stored in multiple places, so first the account of the pair is checked. If there is this amount in the pair, the reserveInput is considered to be the stored value. By subtracting the stored value, the amountIn is calculated. The calculation is done after the gap between the two is reduced by 10%. This is summarized in the CPMM model.

UniswapV2 : v2-periphery/UniswapV2Router02

function swapExactTokensForTokensSupportingFeeOnTransferTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external virtual override ensure(deadline) {
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
    );
    uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
    _swapSupportingFeeOnTransferTokens(path, to);
    require(
        IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
        'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
    );
}
function swapExactETHForTokensSupportingFeeOnTransferTokens(
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
)
    external
    virtual
    override
    payable
    ensure(deadline)
{
    require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
    uint amountIn = msg.value;
    IWETH(WETH).deposit{value: amountIn}();
    assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
    uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
    _swapSupportingFeeOnTransferTokens(path, to);
    require(
        IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
        'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
    );
}
function swapExactTokensForETHSupportingFeeOnTransferTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
)
    external
    virtual
    override
    ensure(deadline)
{
    require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
    );
    _swapSupportingFeeOnTransferTokens(path, address(this));
    uint amountOut = IERC20(WETH).balanceOf(address(this));
    require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
    IWETH(WETH).withdraw(amountOut);
    TransferHelper.safeTransferETH(to, amountOut);
}
  • The reason why "Exact" cannot be done after there are only 3 functions is because:

    • To fix the "output," I need to know in advance what the token is, but I don't know in advance. The token could deduct 10% tax or have some interaction, so the supporting fee doesn't care about the actual calculation and only looks at the beginning and end results.

    • That's why reverse calculation is difficult.

  • The calculation of supporting fee only looks at the later values, so reverse calculation is not possible. That's why normal swaps can cause errors because if tax is deducted, the value of "k" decreases. To solve this problem, supporting fee is applied.

Resource

Subscribe to Zer0Luck
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
Author Address
0x9b399D329a3CDfB…d4117Bf3cC27a39
Content Digest
rnu5BTT8f45lq3Y…pfa710SinXaGoZ8