✅ Follow me: X: @Younsle1 Warpcast: @zer0luck
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.
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
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 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.)
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)
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.
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.
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)
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
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
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.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.
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.
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.
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
uint112
, so it is multiplied by about 1000.sync() and skim() → reserves ≠ balances, tax token
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)
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
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?
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
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;
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);
// 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.
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
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.
//...
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);
require
statement is used to determine if the calculation satisfies the Constant Product Market Maker (CPMM) criteria.// **** 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)
);
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
))));
}
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.// 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);
}
(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.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.
// 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).
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]);
}
// **** 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.
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.