Dissecting the PancakeSwap protocol
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:
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); }
It gets the amount of reserves from the trading pair.
(uint reserveA, uint reserveB) = PancakeLibrary.getReserves(factory, tokenA, tokenB);
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); }
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);
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.
It is a
payable
function, as the user can provide BNBThe contract wraps BNB into Wrapped BNB, which is a BEP-20 token
IWETH(WETH).deposit{value: amountETH}(); assert(IWETH(WETH).transfer(pair, amountETH));
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
.
It transfers the user’s LP tokens to the trading pair contract
IPancakePair(pair).transferFrom(msg.sender, pair, liquidity);
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.
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');
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:
Check that token A and token B are not the same token.
require(tokenA != tokenB, 'Pancake: IDENTICAL_ADDRESSES');
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 valueaddress(0)
.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;