Market Calculations

How to calculate pricing, discount, vesting etc for markets

Pricing & Discount

First, assuming you wish to display USD pricing, you will need to obtain USD prices for your quote and payout tokens. How you do this is up to you, and can vary depending on the token. For example, if both tokens have accurate pricing available on Coingecko or a similar service, you can simply pull the price from their API. In other cases, you may wish to use an oracle or calculate from an LP pair. If your quote token is something like an LP pair or a Balancer Weighted Pool, you will have to manually calculate the value of the token to get accurate market pricing.

You will also need the number of decimals for your payout/quote tokens - if you are simply displaying your markets with a known set of tokens, it makes sense to hardcode these values.

This is a UI issue - the underlying contracts work on the quote/payout ratio, not USD price, so nothing here will affect the actual price at which users can bond, just the usability of your UI. Accordingly, if you wanted to display pricing in EUR, BTC or anything else, you could substitute USD prices for those.

Once you have your pricing information, we recommend the following contract requests:

const [
  currentCapacity,
  marketPrice,
  marketScale,
  maxAmountAccepted,
  marketInfo,
  isLive,
  markets,
  ownerPayoutBalance,
  ownerPayoutAllowance,
] = await Promise.all([
  auctioneerContract.currentCapacity(marketId),
  auctioneerContract.marketPrice(marketId),
  auctioneerContract.marketScale(marketId),
  auctioneerContract.maxAmountAccepted(
    marketId,
    referrerAddress,
  ),
  auctioneerContract.getMarketInfoForPurchase(marketId),
  auctioneerContract.isLive(marketId),
  auctioneerContract.markets(marketId),
  payoutTokenContract.balanceOf(owner),
  payoutTokenContract.allowance(
    ownerAddress,
    tellerAddress,
  ),
]);

The next step is to adjust for market scale and get the discounted price:

The price decimal scaling for a market is split between the price value and the scale value in order to be able to support a broader range of inputs. Specifically, half of it is in the scale and half in the price. To normalize the price value for display, we can add the half that is in the scale factor back to it.

const baseScale = BigNumber.from('10').pow(
  BigNumber.from('36')
    .add(payoutToken.decimals)
    .sub(quoteToken.decimals),
);

const shift = Number(baseScale) / Number(marketScale);
const price = Number(marketPrice) * shift;
const quoteTokensPerPayoutToken = price / Math.pow(10, 36);
const discountedPrice = quoteTokensPerPayoutToken * quoteToken.price;

Here, quoteTokensPerPayoutToken is the exchange rate between the two tokens used by the contract to determine the current bond price. Multiplying this by the quote token's USD price gives us the USD price at which a user can purchase from the market.

Next we can calculate the discount:

let discount = (discountedPrice - payoutToken.price) / payoutToken.price;
discount *= 100;

The 'discount' can also be negative - i.e. purchasing from BondProtocol is currently more expensive than purchasing at market rates. Once the daily capacity has been hit, the discount will be negative until it gradually ticks back to a positive discount.

It is strongly recommended to make this clear in your UI (different colors, warning message etc) to avoid complaints from users who accidentally bonded at a large premium.

Vesting

There are a few options for displaying vesting. First, if you are running an instant swap market, you can just display it as such in the front end. The auctioneer contract has an isInstantSwap(marketId) function, if you are unsure.

In most cases, your market will either have a fixed vesting date, or a fixed vesting term (e.g. 14 days). You can get the vesting period value from the auctioneer contract's terms(marketId) function, which returns 4 values, vesting (uint48) being the 3rd. Since this doesn't change after market creation, you could hardcode it.

If you have a fixed expiration market, vesting will be a timestamp. We use this to create a new JS Date object for the expiration date, which you can then format as desired, for example:

new Date(calculatedMarket.vesting * 1000, 'yyyy-MM-dd');

If you have a fixed term market, vesting will be a duration in seconds. We use the following function to create a human-readable value:

export function longVestingPeriod(seconds: number): string {
  const d = Math.floor(seconds / (3600 * 24));
  const h = Math.floor((seconds % (3600 * 24)) / 3600);
  const m = Math.floor((seconds % 3600) / 60);

  const dDisplay = d > 0 ? d + (d == 1 ? ' day, ' : ' days, ') : '';
  const hDisplay = h > 0 ? h + (h == 1 ? ' hr, ' : ' hrs, ') : '';
  const mDisplay = m > 0 ? m + (m == 1 ? ' min' : ' mins') : '';

  let result = dDisplay + hDisplay + mDisplay;
  if (mDisplay === '') {
    result = result.slice(0, result.length - 2);
  }

  return result;
}

Capacity, Payout & Allowances

To display the current capacity of your market in a human-readable way, you need to adjust the currentCapacity value by your capacity token's decimals - the capacity can be in either quote or payout token depending on how you set up the market, this could be hardcoded if you know the decimal value will be the same across all markets you are displaying:

const decimals = capacityInQuote ? quoteToken.decimals : payoutToken.decimals;
currentCapacity = Number(currentCapacity) / Math.pow(10, decimals);

To calculate the max payout for a market:

const maxPayout = Number(marketInfo.maxPayout) / Math.pow(10, payoutToken.decimals);
const maxPayoutUsd = maxPayout * payoutToken.price;

To calculate the max amount of quote tokens accepted for a single purchase:

const maxAccepted =
  (Number(maxAmountAccepted) - Number(maxAmountAccepted) * 0.005) / 
  Math.pow(10, quoteToken.decimals);

We multiply the result of Number(maxAmountAccepted) - Number(maxAmountAccepted) by 0.005 in order to reduce maxAmountAccepted by 0.5%.

This is due to the fee being slightly underestimated in the contract function.

See comment on https://github.com/Bond-Protocol/bond-contracts/blob/master/src/bases/BondBaseSDA.sol line 764 for more detail.

Since payout tokens are not held by BondProtocol, it is possible that the maximum payout could be limited by either the payout token balance of your owner address, or the spending allowance. We check these, and if one is insufficient, display an error message showing the exact issue. Should you wish to do the same, it is straightforward:

const ownerBalance = Number(ownerPayoutBalance) / Math.pow(10, payoutToken.decimals);
const ownerAllowance = Number(ownerPayoutAllowance) / Math.pow(10, payoutToken.decimals);

Finally, you can optionally use isLive to check the status of your market and hide it from your UI when closed.

Last updated