3

I'm using Web3.py and I'm experiencing something strange.

For the following code (with Pancake Router V2):

from web3 import Web3
from web3.middleware import geth_poa_middleware

web3 = Web3(Web3.HTTPProvider('https://bsc-dataseed1.binance.org:443'))
web3.middleware_onion.inject(geth_poa_middleware, layer=0)

ABI = {"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsOut","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"}

CAKE_ROUTER_V2 = web3.toChecksumAddress('0x10ed43c718714eb63d5aa57b78b54704e256024e')
router_contract = web3.eth.contract(address=CAKE_ROUTER_V2, abi=ABI),

WBNB = web3.toChecksumAddress('0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c')
CAKE = web3.toChecksumAddress('0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82')
KONGSHIBA = web3.toChecksumAddress('0x126f5f2a88451d24544f79d11f869116351d46e1')

print(router_contract.functions.getAmountsOut(1, [WBNB, CAKE]).call())
print(router_contract.functions.getAmountsOut(1, [WBNB, KONGSHIBA]).call())

And I'm getting the following:

[1, 19]
[1, 160]

WBNB and CAKE have 18 decimals and KONGSHIBA has 17.
While CAKE's worth is currently about $27.7, WBNB is $545.41291093
and KONGSHIBA is $0.00000000000000000332.
So I should have got back:

[1, 19]
[1, 16000000000000000000]

Please advise.

TylerH
  • 20,799
  • 66
  • 75
  • 101
EvgenyKolyakov
  • 3,310
  • 2
  • 21
  • 31

2 Answers2

11

The proper way to calculate a token price is by asking the liquidity pool (the pair of this token against the local PEG or some USD token) for the ratio of how much PEG was inserted agains how may tokens (for more details about what a liquidity pool represents, see https://uniswap.org/docs/v2/core-concepts/pools/).

So for python use:

from web3 import Web3
from web3.middleware import geth_poa_middleware # Needed for Binance

from json import loads
from decimal import Decimal


ETHER = 10 ** 18

WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c'
CAKE_ROUTER_V2 = web3.toChecksumAddress('0x10ed43c718714eb63d5aa57b78b54704e256024e')

web3 = Web3(Web3.HTTPProvider('https://bsc-dataseed1.binance.org:443'))
web3.middleware_onion.inject(geth_poa_middleware, layer=0) # Again, this is needed for Binance, not Ethirium

ABI = loads('[{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"token0","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"getPair","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getReserves","outputs":[{"internalType":"uint112","name":"_reserve0","type":"uint112"},{"internalType":"uint112","name":"_reserve1","type":"uint112"},{"internalType":"uint32","name":"_blockTimestampLast","type":"uint32"}],"payable":false,"stateMutability":"view","type":"function"}]')

def get_price(token, decimals, pair_contract, is_reversed, is_price_in_peg):
    peg_reserve = 0
    token_reserve = 0
    (reserve0, reserve1, blockTimestampLast) = pair_contract.functions.getReserves().call()
    
    if is_reversed:
        peg_reserve = reserve0
        token_reserve = reserve1
    else:
        peg_reserve = reserve1
        token_reserve = reserve0
    
    if token_reserve and peg_reserve:
        if is_price_in_peg:
            # CALCULATE PRICE BY TOKEN PER PEG
            price = (Decimal(token_reserve) / 10 ** decimals) / (Decimal(peg_reserve) / ETHER)
        else:
            # CALCULATE PRICE BY PEG PER TOKEN
            price = (Decimal(peg_reserve) / ETHER) / (Decimal(token_reserve) / 10 ** decimals)
        
        return price
        
    return Decimal('0')


if __name__ == '__main__':
    CAKE_FACTORY_V2 = web3.eth.contract(address=CAKE_ROUTER_V2, abi=ABI).functions.factory().call()

    token = web3.toChecksumAddress('0x126f5f2a88451d24544f79d11f869116351d46e1')
    pair = web3.eth.contract(address=CAKE_FACTORY_V2, abi=ABI).functions.getPair(token, WBNB).call()
    pair_contract = web3.eth.contract(address=pair, abi=ABI)
    is_reversed = pair_contract.functions.token0().call() == WBNB
    decimals = web3.eth.contract(address=token, abi=ABI).functions.decimals().call()
    is_price_in_peg = True

    print(get_price(token, decimals, pair_contract, is_reversed, is_price_in_peg), 'BNB')


And for JS use:

var ETHER = Math.pow(10, 18);

var WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
var CAKE_ROUTER_V2 = Web3.utils.toChecksumAddress('0x10ed43c718714eb63d5aa57b78b54704e256024e');

var web3 = new Web3('https://bsc-dataseed1.binance.org:443');

var ABI = [{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"token0","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"getPair","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getReserves","outputs":[{"internalType":"uint112","name":"_reserve0","type":"uint112"},{"internalType":"uint112","name":"_reserve1","type":"uint112"},{"internalType":"uint32","name":"_blockTimestampLast","type":"uint32"}],"payable":false,"stateMutability":"view","type":"function"}];


var get_price = async function(token, decimals, pair_contract, is_reverse, is_price_in_peg) {
    var price,
        peg_reserve = 0,
        token_reserve = 0,
        res = await pair_contract.methods.getReserves().call(),
        reserve0 = res[0],
        reserve1 = res[1];
        
    if (is_reverse) {
        peg_reserve = reserve0;
        token_reserve = reserve1;
    } else {
        peg_reserve = reserve1;
        token_reserve = reserve0;
    }
    
    if (token_reserve && peg_reserve) {
        if (is_price_in_peg) {
            // CALCULATE PRICE BY TOKEN PER PEG
            price = (Number(token_reserve) / Number(Math.pow(10, decimals))) / (Number(peg_reserve) / Number(ETHER));
        } else {
            // CALCULATE PRICE BY PEG PER TOKEN
            price = (Number(peg_reserve) / Number(ETHER)) / (Number(token_reserve) / Number(Math.pow(10, decimals)));
        }
            
        return price;
    }
    
    return Number(0);
};


var token = Web3.utils.toChecksumAddress('0x126f5f2a88451d24544f79d11f869116351d46e1');
var pair = await (await (new web3.eth.Contract(ABI, CAKE_FACTORY_V2))).methods.getPair(token, WBNB).call();
var pair_contract = await new web3.eth.Contract(ABI, pair);
var is_reversed = (await pair_contract.methods.token0().call()) == WBNB;
var decimals = await (await new web3.eth.Contract(ABI, token)).methods.decimals().call();
var is_price_in_peg = true;

console.log(await get_price(token, decimals, pair_contract, is_reversed, is_price_in_peg), 'BNB')

NOTE 1: This applies only for tokens that have liquidity against WBNB. If the liquidity is against some other coin, you have to recursively understand all the prices in that chain and correlate them one to another until you reach WBNB (or any other PEG in other networks).

NOTE 2. According to https://arxiv.org/pdf/2009.14021.pdf :

Expected Execution Price (E[P]): When a liquidity taker issues a trade on X/Y , the taker wishes to execute the trade with the expected execution price E[P] (based on the AMM algorithm and X/Y state), given the expected slippage.

Execution Price (P): During the time difference between a liquidity taker issuing a transaction, and the transaction being executed (e.g. mined in a block), the state of the AMM market X/Y may change. This state change may induce unexpected slippage resulting in an execution price P != E[P].

Unexpected Price Slippage (P − E[P]): is the difference between P and E[P].

Unexpected Slippage Rate ((P − E[P]) / E[P]): is the unexpected slippage over the expected price.

So in our situation, E[P] is the result of our get_price() and P is the result from getAmounsOut() of an amount (1Kwei for example) divided by the amount we provided (1K in this example) and thus we can even calculate the slippage by eventually subtracting P − E[P]

TylerH
  • 20,799
  • 66
  • 75
  • 101
EvgenyKolyakov
  • 3,310
  • 2
  • 21
  • 31
  • @EvgenyKilyakov what do I do with the result of "price" to get the token value in USD? – MariINova Jan 10 '22 at 07:58
  • @MariiNova get the price of [USDT(C or whatever stablecoin), WPEG] and divid both with relation to the decimals ;) – EvgenyKolyakov Jan 11 '22 at 18:47
  • @EvgenyKolyakov what versions of web3 and other libraries are needed to make the code run in python? I am getting the following error (after fixing the fact that you're calling web3 before you instantiate it): AttributeError: 'Web3' object has no attribute 'middleware_onion' – Michael VanDeMar Feb 17 '22 at 09:42
  • @MichaelVanDeMar latest... I didn't include the require/import as it's trivial - `const Web3 = require('web3');` – EvgenyKolyakov Feb 17 '22 at 11:08
  • @EvgenyKolyakov - what I meant was that you called "CAKE_ROUTER_V2 = web3.toChecksumAddress()" before defining "web3 = Web3(Web3.HTTPProvider()", on my machine it definitely threw an error until I moved that first call down. So when you say latest, I am using web3 4.6.0 and python 3.9 and it's saying that there is no middleware_onion in Web3, are you using versions higher or lower than that? This is on Ubuntu 20.04.3 – Michael VanDeMar Feb 18 '22 at 02:17
  • 1
    @EvgenyKolyakov Ok, I finally figured out that I did not in fact have the latest web3, and was able to fix it with "pip install 'web3==5.28.0'". I have no idea why it was installing 4.6.0 by default, but it works now. Thanks! – Michael VanDeMar Feb 18 '22 at 09:04
  • @EvgenyKolyakov using your example with this token 0x05ad901cf196cbDCEaB3F8e602a47AAdB1a2e69d and some others I get the error: price = (Decimal(peg_reserve) / ETHER) / (Decimal(token_reserve) / 10 ** decimals) 3.392801237050211786537500406E-12 – MariINova Mar 08 '22 at 13:36
  • @MariINova ‍♂️ ‘3.39E-12’ is not an error but a number which means ‘3.39 * 10^-12’ – EvgenyKolyakov Mar 10 '22 at 05:18
-2

Try user toChecksumAddress in WBNB to:

WBNB = w3.toChecksumAddress('0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c')

TylerH
  • 20,799
  • 66
  • 75
  • 101
Dubiella
  • 29
  • 3