Breaking Down the Uniswap V2 Swap Function
RareSkills
We are the graduate school of web3 engineering. Our 5-month engineering fellowship is the most advanced and rigorous.
Uniswap V2’s swap function is cleverly designed, but many devs find its logic counterintuitive the first time they encounter it. This article explains how it works in depth.
Here is the code reproduced below:
Admittedly, this is a wall of code, but let’s break it down.
Using these observations, we will make sense of this function one feature at a time.
Flash Borrowing
Users do not have to use the swap function for trading tokens, it can be used purely as a flash loan.
The borrowing contract simply requests the amount of tokens they wish to borrow (A) without collateral and they will be transferred to the contract (B).
The data that should be provided with the function call is passed in as a function argument (C), and this will be passed to a function that implements
IUniswapV2Callee. The function uniswapV2Call must pay back the flash loan plus the fee or the transaction will revert.
Swap requires using a smart contract
If a flash loan is not used, the incoming tokens must be sent as part of calling the swap function.
It should be clear that only a smart contract is able to interact with a swap function, because an EOA cannot simultaneously send the incoming ERC20 tokens and call swap in one transaction without the aid of another smart contract.
Measuring the amount of incoming tokens
The way Uniswap V2 “measures” the amount of tokens sent in is done on line 176 and 177, marked with the yellow box below.
Remember, reserve0 and reserve1 are not updated inside this function. They reflect the balance of the contract before the new set of tokens were sent in as part of the swap.
One of two things can happen for each of the two tokens in the pair:
The way the code determines which situation happened with the following logic:
If it measures a net decrease, the ternary operator returns zero, otherwise it will measure the net gain of tokens in.
It is always the case that _reserveX > amountXOut because of the require statement on line 162.
Some examples.
Conclusion: amount0In and amount1In will reflect the net gain if there was a net gain for the token, and they will be zero if there was a net loss of that token.
领英推荐
Balancing XY = K
Now that we know how many tokens the user sent in, let’s see how to enforce XY = K.
The code again is
Uniswap V2 charges a hardcoded 0.3% per swap, which is why we see the numbers 1000 and 3 at play, but lets simplify this by changing it to the case where Uniswap V2 charged no fees. This means we can remove the .sub(amountXIn.mul(3)) term and not multiply by 1000 on lines 180 to 181 or 1000**2 on line 182.
The new code would be
This is saying
K is not really constant
It’s a bit misleading to say “K remains constant” even though the AMM formula is sometimes referred to as a “constant product formula.”
Think about it this way, if someone donated tokens to the pool and changed the value of K, we wouldn’t want to stop them because they made us liquidity providers richer, right?
Uniswap V2 doesn’t prevent you from “paying too much” i.e. transferring in too many tokens in during the swap (this is related to one of the safety checks, which we will get to later).
We would be upset if there was a net loss in the pool, which is what the require statement is checking. If K gets larger, it means the pool got larger, and as liquidity providers, that’s what we want.
Accounting for fees
But not only do we want K to get larger, we want it to get larger by at least an amount that enforces the 0.3% fee.
Specifically, the 0.3% fee applies to the size of our trade, not the size of the pool. It only applies to the tokens that go in, not on the tokens that go out. Some examples:
Observe that if we flash borrow one of the tokens, it results in the same fee as swapping that token for the same amount. You pay fees on tokens in, not on tokens out. But if you don’t put tokens in, there is no way for you to borrow or swap.
Remember, reserve0 and reserve1 represent the old balances, and balance0 and balance1 represent the updated balances.
With that in mind, let’s write the code below should be self-explanatory. The multiplying by 1000 and 3 is to simply accomplish “fractional” multiplication since it cancels out in the end.
The code is accomplishing the following formula:
That is, the new balance must increase by 0.3% of the amount in. In the code, the formula is scaled by multiplying each term by 1,000 because Solidity doesn’t have floating point numbers, but the math formula shows what the code is trying to accomplish.
Updating Reserves
Now that the trade is completed, then the “previous balance” must be replaced with the current balance. This happens in the call to the _update() function at the end of swap().
The _update() function
There is a lot of logic here to handle the TWAP oracle, but all we care about for now is lines 82 an 83 where the storage variables reserve0 and reserve1 are updated to reflect the changed balances. The arguments reserve0 and reserve1 are used to update the oracle, but they are not stored.
Safety Checks
There are two things that can go wrong:
These circumstances can happen if someone frontruns a transaction (intentionally or not) and changes the ratio of assets in the pool in an undesirable direction.
Learn more with RareSkills
This article is part of our advanced Solidity Bootcamp. Please check our RareSkills website to learn more.
Solidity, Liquidity & Gas
1 年Great series!!