4

Hi i am trying to understand the reasoning behind the swap function of a pancake pair on the pancakeSwap DEX and the checks it does.

this is the function :

require(amount0Out > 0 || amount1Out > 0, 'Pancake: INSUFFICIENT_OUTPUT_AMOUNT');
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'Pancake: 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, 'Pancake: 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) IPancakeCallee(to).pancakeCall(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, 'Pancake: INSUFFICIENT_INPUT_AMOUNT');
        { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
        uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(2));
        uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(2));
        require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'Pancake: K');
        }

        _update(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);

Q1 : in the code above there is a distinction between the balance0 and reserveO as well as balance1 and reserve1 aren't they the same? is the balance of the pair contract an accumulation of fees?

Q2 : I would like to also know about amountOIn, and why it is calculated that way?

Q3 : What is the Pancake: K error and why is it calculated that way?

3 Answers 3

6
+100

balance0 and balance1 are local variables of functions mint, burn and swap while reserve0 and reserve1 are globally declared variables.

why there is a need for computing balance and reserve?

As mentioned in UniswapV2 whitepaper

Another complication is that it is possible for someone to send assets to the pair contract—and thus change its balances and marginal price—without interacting with it, and thus without triggering an oracle update. If the contract simply checked its own balances and updated the oracle based on the current price, an attacker could manipulate the oracle by sending an asset to the contract immediately before calling it for the first time in a block. If the last trade was in a block whose timestamp was X seconds ago, the contract would incorrectly multiply the new price by X before accumulating it, even though nobody has had an opportunity to trade at that price. To prevent this, the core contract caches its reserves after each interaction, and updates the oracle using the price derived from the cached reserves rather than the current reserves. In addition to protecting the oracle from manipulation, this change enables the contract re-architecture described below in section 3.2.

To put it in simple words,

At each swap call, first reserves for both the tokens are fetched, then tokens are transferred optimistically (to allow flash swaps), then there are conditions to check that swap was legal including fees and adjusted balance after a change in the quantity of tokens. Finally, the _update function is called which updates reserves according to new balances & old reserves after swapping.

Reserves are updated on mint, burn, and sync as well.

If there was only no provision updating reserves only after AMM functions, it leads to an attack vector of transferring tokens (which are part of pair) directly without using the AMM function and potentially making currency cheaper and causing imbalance due to which it cannot be used as an oracle.

how are amount0In and amount1In calculated?

amount0In and amount1In are local variables that keep tracking of what quantity of each token inflow during the particular swap. In simple terms, it is checking if the balance (after transferring/receiving) is greater than reserve by amount0Out (this is the case if that token was deposited because reserves are not updated after the last swap) then amount0In is balance0 (total after inflow) + amount0Out(outflow) - _reserve0(last_updated).

Usually, only either of the tokens of pair is sent out of AMM, and in that case, amount0Out or amount1Out, let say amount0Out is 0, so amount0In becomes balance0 (total after inflow) - _reserve0(last_updated). this is not always the case and also both cannot be 0, but it easier to visualize.

If balance(after transferring/receiving) is not greater than reserve by amount0Out then we can be sure that the net token quantity of that was transferred out and hence amount0In would be 0 in that case. This does not mean that a specific token was not transferred to AMM but instead more quantity was transferred out from AMM than that of transferred into AMM. in either way, if transfer out is more than transfer in, it is taken as 0 as it will not have an impact on fees and oracles as they are calculated on net changes!

what is the Pancake: K(or UniswapV2: K) error and why is it calculated that way?

From UniswapV2 docs

The Uniswap constant product formula is “X * Y = K”. Where X and Y represent the respective reserve balances of two ERC-20 tokens, and “K” represents the product of the reserves. It is this “K” to which the “K” error refers.

In essence, the “K” error means that a trade was attempted that somehow left the trading pair with less reserves than should be there, and as a result the transaction is reverted.

This can have a few different causes.

1
  • can amount0out and amount1out both have non 0 < values?
    – darren z
    Commented Dec 29, 2021 at 23:52
4

Here is the TL;DR version.

Q1 balance0 and balance1 vs reserve0 and reserve1

reserve0 and reserve1 are the balances of each tokens in the pair contract just before the swap transaction is processed.

In contrast, balance0 and balance1 are the balances of each tokens in the pair contract after the swap transaction has been processed.

The difference between the two is the increase or decrease of each token due to the swap.

Q2 amount0In calculation

This variable is the amount of token that was sent to the pair contract. If a user withdraws an amount of token 1 and sends an amount of token 0 (a regular swap), then amount0In would simply equal the amount of token 0 sent to the pair contract.

However, it is possible to withdraw token 0 and send back token 0 to the pair (a flash loan of a single token, without swap). The calculation is a bit more complicated to reflect that possibility.

Q3 Pancake: K

All Uniswap forks use the same concept of constant product formula: x*y=k.

reserve0 times reserve1 should always be equal to a constant k. This is what enforces the fair pricing of the swap.

The require statement simply checks that the product of the balances of each tokens after the swap, adjusted for the fee, is still equal to the k constant before the swap.

0

I'm adding some details about the calculation of amount0In mentioned by @Undead8.

Normal Swaps

In this case, one of two parameters amount0Out and amount1Out is always zero when they were passed to the swap function, this can be confirmed by the lines of the wrapper function _swap in UniswapV2Router01:

(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));

IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(amount0Out, amount1Out, to, new bytes(0));

amountIn is the amount of token that a trader sells to a liquidity pool, after that the balance of the ingested token is increased comparing to the reserved that hasn't been updated yet, thus we have:

img

In this formula it could be amount0In with corresponding balance0 and reserve0, or vice versa.

Flash Swaps

Flash swaps is similar to flash loan but it allows traders to pay back loan in either trading token. The traders deploy a contract to directly issue a call to the swap function and pay the debt along with swapping fee back to liquidity pool within IUniswapV2Callee(to).uniswapV2Call() in a same transaction.

In this case, traders can do not only borrow one token but also can borrow two assets at the same time:

amount0Out = 1 * 10**18;
amount1Out = 1 * 10**18;
pair.swap(amount0Out, amount1Out, to, data);

This is why we have:

if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);

If we take amountIn as the amount of token that trader has paid back, thus we have:

So the amountIn is:

Does this look familiar? Yes, that is what the statements had shown us:

uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;

This formula also works for normal swaps, because on input side the amountOut is always zero, we could simply put the zero into the formula:

This is exactly the identical formula we have shown in the case of normal swaps.

3
  • Hello Sayoko, I am wondering the flash Swaps part. We can call the swap function directly putting amount0Out and amount1Out values. It looks like I can put any valune in amountOut and transfer the token as much as I want since " if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); ". What if I put higher value than the amount is supposed to be swapped, is it still working? For instance, the swapped amount should be 100 but I put 10,000 in amountOut0 _safeTransfer(_token0, me, 10,000); is working? I cannot find any prevention from this? Could you help me?
    – DHC
    Commented Oct 16, 2023 at 2:19
  • @DHC I couldn't figure out what 'swapped amount' means, but I assume you were refered to the total balance (or reserve) of one of ERC-20 tokens in a UniswapV2Pair. (1) There are a few checks at the beginning of swap function, the most crucial check is to examine amountOut to make sure its value cannot exceed the balance that the UniswapV2Pair actually holds: require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
    – Sayoko
    Commented Oct 17, 2023 at 5:20
  • @DHC (2) Even though without this precaution, it would still safe to rely on the result of _safeTransfer() function which internally issues a call into transfer() function of the ERC-20 token contract, depending on the specific implementation of the transfer() function of the contract, but almost all of contracts will fail if users have no sufficient assets to be withdrawn. Hope this would help.
    – Sayoko
    Commented Oct 17, 2023 at 5:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.