DEV Community

Amree Zaid
Amree Zaid

Posted on • Updated on

Code Reading: Calculate WBNB per Token

Code Reading: Calculate WBNB per Token

Given a token, how do calculate its value against WBNB?

Someone already solved this for us, but let us try to understand what the code is actually doing.

// src/exchange/pricing.ts

/**
 * Search through graph to find derived BNB per token.
 * @todo update to be derived BNB (add stablecoin estimates)
 **/
export function findBnbPerToken(token: Token): BigDecimal {
  if (token.id == WBNB_ADDRESS) {
    return ONE_BD;
  }
  // loop through whitelist and check if paired with any
  for (let i = 0; i < WHITELIST.length; ++i) {
    let pairAddress = factoryContract.getPair(Address.fromString(token.id), Address.fromString(WHITELIST[i]));
    if (pairAddress.toHex() != ADDRESS_ZERO) {
      let pair = Pair.load(pairAddress.toHex());
      if (pair.token0 == token.id && pair.reserveBNB.gt(MINIMUM_LIQUIDITY_THRESHOLD_BNB)) {
        let token1 = Token.load(pair.token1);
        return pair.token1Price.times(token1.derivedBNB as BigDecimal); // return token1 per our token * BNB per token 1
      }
      if (pair.token1 == token.id && pair.reserveBNB.gt(MINIMUM_LIQUIDITY_THRESHOLD_BNB)) {
        let token0 = Token.load(pair.token0);
        return pair.token0Price.times(token0.derivedBNB as BigDecimal); // return token0 per our token * BNB per token 0
      }
    }
  }
  return ZERO_BD; // nothing was found return 0
}
Enter fullscreen mode Exit fullscreen mode

Source: https://git.io/Jno0J

For this code walkthrough, we will assume that we are passing CAKE as the params. More information of this token can be found here.

if (token.id == WBNB_ADDRESS) {
  return ONE_BD;
}
Enter fullscreen mode Exit fullscreen mode

This is quiet straightforward. If we are given WBNB, then we will just return 1 as the rate. E.g: Given WBNB, it will always return 1 as it is the same token. But we are passing CAKE here, so, we will be proceeding to the next line.

for (let i = 0; i < WHITELIST.length; ++i) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

WHITELIST here refers to:

let WHITELIST: string[] = [
  "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", // WBNB
  "0xe9e7cea3dedca5984780bafc599bd69add087d56", // BUSD
  "0x55d398326f99059ff775485246999027b3197955", // USDT
  "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", // USDC
  "0x23396cf899ca06c4472205fc903bdb4de249d6fc", // UST
  "0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c", // BTCB
  "0x2170ed0880ac9a755fd29b2688956bd959f933f8", // WETH
];
Enter fullscreen mode Exit fullscreen mode

Source: https://git.io/JnKXF

let pairAddress = factoryContract.getPair(Address.fromString(token.id), Address.fromString(WHITELIST[i]));
if (pairAddress.toHex() != ADDRESS_ZERO) {
  // ..
}
Enter fullscreen mode Exit fullscreen mode

Assuming we are passing CAKE token, this means, the code in the loop will only continue if there is no matching pair. The order of the tokens in the pair doesn't really matter in this case.

factoryContract.getPair(Address.fromString(token.id), Address.fromString(WHITELIST[i]))
Enter fullscreen mode Exit fullscreen mode

We can try this manually with the factory address of PancakeSwap V2 at BscScan. Just scroll to the getPair method and input the tokens that we want. If there are multiple pairs with similar tokens, it will return the first one it found based on the index (I may need to check the factory code to confirm this).

Let’s go to the next line of code

let pair = Pair.load(pairAddress.toHex());
Enter fullscreen mode Exit fullscreen mode

Nothing much here. This will just load the pair that we found earlier to pair. However, it is good to know that we will also get extra information such as the reserveBNB, derivedBNB and others. These information is not available on the network itself but it is something that subgraph stored. We can see all the attributes here.

Now let us go to an important piece of the code in the function:

if (pair.token0 == token.id && pair.reserveBNB.gt(MINIMUM_LIQUIDITY_THRESHOLD_BNB)) {
  // ...
}
if (pair.token1 == token.id && pair.reserveBNB.gt(MINIMUM_LIQUIDITY_THRESHOLD_BNB)) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

As we can see here, the code is trying to find out token is in token0 or token1. To those who don't know, we can get the token0 and token1 in the pair page at BscScan, an example would be this page for CAKE-WBNB.

Since we are passing CAKE in this walkthrough, it will be found in the first if as for the CAKE-WBNB pair, the token0 is equal to CAKE. So, that should have solved this part:

pair.token0 == token.id
Enter fullscreen mode Exit fullscreen mode

Let us move to the second condition:

pair.reserveBNB.gt(MINIMUM_LIQUIDITY_THRESHOLD_BNB)
Enter fullscreen mode Exit fullscreen mode

The value of MINIMUM_LIQUIDITY_THRESHOLD_BNB is actually 10. Source is here.

But what about pair.reserveBNB, what's the value? To answer that, we need to find out where it's actually being set first.

// src/exchange/core.ts

pair.reserveBNB =
  pair.reserve0.times(token0.derivedBNB as BigDecimal).plus(
    pair.reserve1.times(token1.derivedBNB as BigDecimal)
  );
Enter fullscreen mode Exit fullscreen mode

Source: https://git.io/Jn6ft

Now we need to know where token0.derivedBNB is from, which is:

// src/exchange/core.ts

let t0DerivedBNB = findBnbPerToken(token0 as Token);
token0.derivedBNB = t0DerivedBNB;
token0.save();

let t1DerivedBNB = findBnbPerToken(token1 as Token);
token1.derivedBNB = t1DerivedBNB;
token1.save();

// ...

pair.reserveBNB =
  pair.reserve0.times(token0.derivedBNB as BigDecimal)
    .plus(
      pair.reserve1.times(token1.derivedBNB as BigDecimal)
     );
Enter fullscreen mode Exit fullscreen mode

Source: https://git.io/Jn6f6

Wait, it’s actually calling the same function? Isn’t this going to cause infinite loop? Actually NO, as this function is being called in different time. To be specific, it will be called whenever the contract emit SYNC event. So, derivedBNB will always be updated with a value when that event happened. Source: https://git.io/Jn6TR

So, we can can assume when findBnbPerToken is called, the derivedBNB value is already there. This is actually part of the code that confuses me as it feels like the value will never increase. But here is my guest:

  • pair.reserveBNB, token0.derivedBNB and token1.derivedBNB has initial value of 0
  • If we take an example of CAKE/WBNB pair, findBnbPerToken will always return 0 for CAKE as it doesn't meet the MINIMUM_LIQUIDITY_THRESHOLD_BNB until WBNB reserve which is also pair.reserve1 in the pair has more than MINIMUM_LIQUIDITY_THRESHOLD_BNB or 10
  • This also means this code block depends entirely on the WBNB or token1.derivedBNB to have non zero value at first:
// src/exchange/core.ts

pair.reserveBNB = pair.reserve0
    .times(token0.derivedBNB as BigDecimal)
    .plus(pair.reserve1.times(token1.derivedBNB as BigDecimal));
Enter fullscreen mode Exit fullscreen mode

Alright, let us go back up again to this condition again:

if (pair.token0 == token.id && pair.reserveBNB.gt(MINIMUM_LIQUIDITY_THRESHOLD_BNB)) {
  // ...
}
if (pair.token1 == token.id && pair.reserveBNB.gt(MINIMUM_LIQUIDITY_THRESHOLD_BNB)) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Assuming the second condition has been met, we will go inside that if:

let token1 = Token.load(pair.token1);
// return token1 per our token * BNB per token 1
return pair.token1Price.times(token1.derivedBNB as BigDecimal);
Enter fullscreen mode Exit fullscreen mode

Source: https://git.io/JniYJ

I would say the final piece of the code is the most important part here. The first line is just to load the token information and in this case, we are going to load the other token. If the token that we passed (CAKE) is in the first position, then we are going to load the other token which is WBNB in this case.

Once we got WBNB loaded, we can calculate the value of this token agains WBNB. I've already explained about .derivedBNB attribute before, but for this case, it's quiet straightforward as we are going to pass WBNB which will get us 1 as it is WBNB/WBNB.

So, we need to figure out the pair.token1Price now which is coming from:

// src/exchange/core.ts

if (pair.reserve0.notEqual(ZERO_BD))
  pair.token1Price = pair.reserve1.div(pair.reserve0);
else
  pair.token1Price = ZERO_BD;
Enter fullscreen mode Exit fullscreen mode

Source: https://git.io/JniOs

In this case, we want to know the value of WBNB/CAKE and we are going to use the reserve amount. Reserve amounts can be retrieved from the pair's contract getReserves function which can be seen in this pair's contract.

After that, we will get the value of 1 WBNB = X CAKE. Do remember the output is in CAKE and not WBNB. After that we can get of CAKE token in WBNB by X CAKE * CAKE/WBNB


To be honest, I am still not satisfied with this explanation. I will update this post in the future if I have a better way to do it, most probably with a real example. However, I think these are some of the keys that helped me (hopefully it will help you as well):

  • findBnbPerToken will produce output of TOKEN_X/WBNB with TOKEN_X as the argument
  • To simplified this, we can ignore this condition first:
pair.reserveBNB.gt(MINIMUM_LIQUIDITY_THRESHOLD_BNB)
Enter fullscreen mode Exit fullscreen mode
  • Most of the work will be done on the other token. If TOKEN_X is the first one, then we'll be doing calculation on the second token.

Ugh, I’m still not satisfied with this post. Let us consider this post as v0.0.1 😅

Examples

TOKEN/WBNB

WBNB/TOKEN

TOKEN/WHITELIST_TOKEN_NOT_WBNB

TOKEN/NO_WHITELIST

Discussion (0)