Dissecting DeFi Protocols

Share this post

Dissecting the PancakeSwap protocol

0xkowloon.substack.com

Dissecting the PancakeSwap protocol

0xkowloon
Mar 12, 2021
2
Share this post

Dissecting the PancakeSwap protocol

0xkowloon.substack.com
Photo credit: https://docs.pancakeswap.finance

This is the beginning of a series of dissection of different DeFi protocols’ codebase.

PancakeSwap is a fork of the Uniswap protocol that is deployed on the Binance Smart Chain and we don’t need to explain what it does. This introduction should be applicable to most token swap protocols as most of them forked from Uniswap. Also, note that since it is a fork of a protocol running on the Ethereum blockchain, many method and variable names are still using the word ETH. It has two main repositories on GitHub, namely pancake-swap-core and pancake-swap-periphery.

The contract PancakeRouter.sol in pancake-swap-periphery is the contract which DeFi users interact with, whether he wants to provide liquidity or to swap tokens.

Adding liquidity

To provide liquidity, you call the method addLiquidity, which does the following:

  1. It creates a trading pair via its factory if the pair does not exist already.

    if (IPancakeFactory(factory).getPair(tokenA, tokenB) == address(0)) {
        IPancakeFactory(factory).createPair(tokenA, tokenB);
    }
  1. It gets the amount of reserves from the trading pair.

    (uint reserveA, uint reserveB) = PancakeLibrary.getReserves(factory, tokenA, tokenB);
  2. It calculates the amount of token A and token B to take from the user. If the reserve is empty, then the amounts will be whatever the user provided. Otherwise, the contract tries to get a quote for the token A and token B pair. Note that if the a token’s optimal amount is less than the a token’s minimum amount, the contract will fail with the error “INSUFFICIENT_X_AMOUNT“.

    uint amountBOptimal = PancakeLibrary.quote(amountADesired, reserveA, reserveB);
    if (amountBOptimal <= amountBDesired) {
        require(amountBOptimal >= amountBMin, 'PancakeRouter: INSUFFICIENT_B_AMOUNT');
        (amountA, amountB) = (amountADesired, amountBOptimal);
    } else {
        uint amountAOptimal = PancakeLibrary.quote(amountBDesired, reserveB, reserveA);
        assert(amountAOptimal <= amountADesired);
        require(amountAOptimal >= amountAMin, 'PancakeRouter: INSUFFICIENT_A_AMOUNT');
        (amountA, amountB) = (amountAOptimal, amountBDesired);
    }
  3. It transfers token A and token B from the msg.sender to the trading pair.

    TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
    TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
  4. Finally, it mints LP tokens to the address provided in the argument, which should be the user.

    liquidity = IPancakePair(pair).mint(to);

You can also provide liquidity by providing BNB and a BEP-20 token using the method addLiquidityETH, which is very similar to addLiquidity with very minor differences.

  1. It is a payable function, as the user can provide BNB

  2. The contract wraps BNB into Wrapped BNB, which is a BEP-20 token

    IWETH(WETH).deposit{value: amountETH}();
    assert(IWETH(WETH).transfer(pair, amountETH));
  3. Finally, it checks if there are leftover BNBs to be refunded back to the user.

    if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);

Removing liquidity

A LP token holder can choose to remove his liquidity anytime by calling the method removeLiquidity.

  1. It transfers the user’s LP tokens to the trading pair contract

    IPancakePair(pair).transferFrom(msg.sender, pair, liquidity);
  2. It burns the user’s LP tokens and sends token A and token B back to the user. The transfers happen inside the IPancakePair contract.

    (uint amount0, uint amount1) = IPancakePair(pair).burn(to);

    Inside the IPancakePair burn function:

    uint balance0 = IERC20(_token0).balanceOf(address(this));
    uint balance1 = IERC20(_token1).balanceOf(address(this));
    ...
    amount0 = liquidity.mul(balance0) / _totalSupply;
    amount1 = liquidity.mul(balance1) / _totalSupply;
    ...
    _burn(address(this), liquidity);
    _safeTransfer(_token0, to, amount0);
    _safeTransfer(_token1, to, amount1);

If the user wants to remove BNB liquidity, the method removeLiquidityETH can be called instead, which behaves in the same way as removeLiquidity, except it also unwraps WBNB into BNB.

IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);

I noticed that it is also possible to remove liquidity “with permit”, which originated from EIP-2612. Users can provide a signature to permit the contract to spend his tokens without submitting an approve transaction on-chain, hence saving on gas fees.

function removeLiquidityWithPermit(..., uint8 v, bytes32 r, bytes32 s)

v, r, s are the values for a transaction’s signatures. r and s are outputs of an ECDSA signature and v is the recovery id.

IPancakePair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);

Swapping tokens

There are several methods for swapping tokens and I am not going to go through all of them as they are all very similar in terms of behavior.

swapExactTokensForTokens

This method accepts amountIn and amountOutMin, which the amountOutMin is the minimum acceptable amount of tokens to receive by the user. In a trading pool, there are often slippages and users have the option to decide how much of a slippage they are willing to accept.

  1. It calculates the output amount and makes sure the amount is not less than the minimum amount. Otherwise, the error INSUFFICIENT_OUTPUT_AMOUNT is raised.

    amounts = PancakeLibrary.getAmountsOut(factory, amountIn, path);
    require(amounts[amounts.length - 1] >= amountOutMin, 'PancakeRouter: INSUFFICIENT_OUTPUT_AMOUNT');
  2. It transfers the input amount to the trading pool.

    TransferHelper.safeTransferFrom(path[0], msg.sender, PancakeLibrary.pairFor(factory, path[0], path[1]), amounts[0]);

    path is the route where the input token travels to reach the output token, especially for the case where there isn’t a trading pool for the two.

    for (uint i; i < path.length - 1; i++) {
        (address input, address output) = (path[i], path[i + 1]);
        ....
    }

    In the method _swap, it loops through the path and keeps swapping between tokens along the path until it reaches the destination. Let’s say if we want to swap CAKE for SUSHI and there isn’t a CAKE-SUSHI pair, but there is a CAKE-BNB and SUSHI-BNB pair. The function will swap CAKE for BNB and then BNB for SUSHI. That’s why it is more expensive than directly swapping between BNB and whatever BEP-20 tokens as it involves more procedures.

    // If we haven't reached the destination, then transfer tokens to the next pair. Otherwise, transfer tokens to the user.
    address to = i < path.length - 2 ? PancakeLibrary.pairFor(factory, output, path[i + 2]) : _to;
    IPancakePair(PancakeLibrary.pairFor(factory, input, output)).swap(amount0Out, amount1Out, to, new bytes(0));

PancakeFactory.sol

The PancakeFactory is a factory contract that creates trading pairs. The method createPair does the following:

  1. Check that token A and token B are not the same token.

    require(tokenA != tokenB, 'Pancake: IDENTICAL_ADDRESSES');
  2. Check that the trading pair does not already exist.

    require(getPair[token0][token1] == address(0), 'Pancake: PAIR_EXISTS');

    In Solidity, there is no such thing as null and it is represented by the value address(0).

  3. It creates the trading pair and pushed the pair’s address into the contract’s storage.

    IPancakePair(pair).initialize(token0, token1);
    getPair[token0][token1] = pair;
    getPair[token1][token0] = pair; // populate mapping in the reverse direction
    allPairs.push(pair);

PancakePair.sol

PancakePair is the trading pair contract that holds the two-sided liquidity. I have briefly talked about how other contracts interact with this contract, but I will briefly talk about this contract’s logic.

Minting LP tokens

The amount of LP tokens to be minted is calculated by the following formula

// If the total supply is 0
// liquidity = (√amount0 x amount1) - 1,000
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);

// If the total supply is not 0
// Liquidity is equal to the minimum of
// amount0 x total supply ÷ reserve0
// and
// amount1 x total supply ÷ reserve1
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);

After minting the LP tokens, it updates price0CumulativeLast, price1CumulativeLast and kLast based on the XYK formula with timestamps taken into account.

price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
...
kLast = uint(reserve0).mul(reserve1);

Following up on the XYK formula, the quote for each trade is calculated in PancakeLibrary with the formula

amountB = amountA.mul(reserveB) / reserveA;
Share this post

Dissecting the PancakeSwap protocol

0xkowloon.substack.com
Comments
TopNewCommunity

No posts

Ready for more?

© 2023 0xkowloon
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing