Pricing model does not match claimed functionality
Description
The BondingViralityController contract allows users to buy and sell tokens associated to a hashtag. The description of the project suggests that the price of the token increases when the hashtag's virality score increases. However, when the virality score increases, only the buy price (the price at which users can buy the token from the contract) increases accordingly, while the sell price (the price at which users can sell their tokens to the contract) decouples from the virality score and depends instead only on the value locked in the contract allocated to this token (the liquidity) and the token supply. In particular, when buying after a rise in virality, the buyer's position will be at an immediate loss, as sell price will be below buy price. Further increases in the virality score will not change the sell price; hence, such a buyer will be entirely dependent on investments by further buyers (facing the same conditions) in order to break even or make a profit.
Terminology and assumptions
We will now explain this in more detail. While every buy/sell costs a 5% fee, we will ignore this in the following explanation (that is, pretend that the fee is 0%), as the fee only makes it more difficult for users to break even or make a profit. Another thing we will ignore is rounding errors. We only consider a single hashtag token, so when we refer to, for example, the contract's liquidity, this will always implicitly mean the contract's liquidity for the token under consideration.
A token's supply is how many tokens have been minted and are in circulation. The initial supply is 10,000 tokens. Users can buy and sell tokens from and to the contract, paying and receiving payment in a payment token P such as USDB. The contract's balance of P held with regards to the hashtag token (i.e., the amount the contract made from users buying tokens, minus the amount the contract paid out when users sell tokens back to the contract) is called the liquidity.
Base price
Buy and sell prices are calculated by the contract's getBuyPrice
and getSellPrice
functions, respectively. Both use the getPrice
function internally. We will call the three prices the buy price, sell price, and base price, respectively. The getPrice
function is defined as follows:
function getPrice(uint256 newSupply, uint16 score) public view returns (uint256) {
uint256 bondingCurvePrice = initialPrice * (newSupply ** exponent) / (initialSupply ** exponent);
return bondingCurvePrice * score;
}
Here, initialPrice
, initialSupply
, and exponent
are constants, with the latter having a value of 3
. The base price is thus of the form , where is some constant.
Buy price
The buy price is then defined by the following function:
function getBuyPrice(address token, uint256 amount) public view returns (uint256) {
HashtagData memory data = registeredTokens[token];
uint256 supplyCeiling = data.supply + amount;
uint256 i = data.supply;
uint256 totalPrice;
while (i < supplyCeiling) {
unchecked { i++; }
totalPrice += getPrice(i, data.viralityScore);
}
return totalPrice;
}
Thus, if the current supply is , then buying one token, which would bring the supply to , will cost exactly getPrice(S+1, viralityScore)
— so . Buying multiple tokens at once costs the same as buying them one at a time (recall that we are assuming transaction fees are zero here). We can summarize this as buying a token costing the token's base price, and this price is proportional to the virality score.
Sell price
Let us now consider the sell price:
function getSellPrice(address token, uint256 amount) public view returns (uint256) {
HashtagData memory data = registeredTokens[token];
if (data.supply - amount < initialSupply) revert NotAllowed();
uint256 supplyFloor = data.supply - amount;
uint256 i = initialSupply;
uint256 totalPrice;
uint256[] memory prices = new uint256[](amount);
uint256 j;
while (i < data.supply) {
uint256 price = getPrice(i+1, data.viralityScore);
if (i >= supplyFloor && i < data.supply) {
prices[j] = price;
unchecked { j++; }
}
if (i < data.supply) {
totalPrice += price;
}
unchecked { i++; }
}
// account for contract liquidity
uint256 finalTotalPrice;
uint256 k = 0;
while (k < j) {
uint256 pctAllocation = prices[k] * PCT_PRECISION / totalPrice;
uint256 allocationPrice = data.liquidity * pctAllocation / PCT_PRECISION;
finalTotalPrice += prices[k] < allocationPrice ? prices[k] : allocationPrice;
unchecked { k++; }
}
return finalTotalPrice;
}
Let us denote the initial supply by , the supply after selling , and the current supply . Furthermore, we denote by the current virality score and by , liquidity. Then we will have
Let us define . Note that if so far, there were no sells for this token, and all tokens have been bought when the virality score was , then we would have . With this definition of , we obtain the following, continuing from the calculations above.
This leaves us two cases. If , we obtain
Thus, if , the sell price for a token is the same as the buy price.
If instead , then we obtain
Thus, if , then the sell price will otherwise be independent of . Instead, the sell price will be what the buy price would be at the lower virality score instead of at the actual virality score . This implies that when the virality score is rising, as soon as it rises above , the sell price will decouple from buy price and remain constant (supply staying equal), while buy price continues to rise proportionally with .
A concrete example
Let us consider a concrete scenario. Let us assume that virality was 100 so far, and 10,000 tokens have been bought by users in total, so that (including the initial supply of 10,000) the supply is now 20,000. Note that we can set the constant to a positive value of our choosing without loss of generality (different values amount only to different scalings of the denomination of prices in P). We choose here to get numbers that are easy to state.
If user A bought the last token (the one that made the supply go from 19,999 to 20,000), then they paid 8 for it. Assume that now virality increases to 1,000. Thus now , but we still have , so the sell price will not change, while the buy price will be 10 times as high as before. Selling now, user A would thus only be paid 8.00, making no profit. To make a profit, user A is dependent on increasing. As , this will happen if the total supply increases — so if other users buy at this higher virality .
Let us now consider a possible user B that buys a token in this situation. Buying a token now will cost 80.01. If user B were to sell that token again immediately after buying it, they would still only get paid 8.02 for it (the ratio between buy and sell price is not quite 10 anymore, as user B buying at a virality score higher than increased a little). User B's position will thus start out at a loss of 89.98%.
Under what conditions would user B be able to break even when selling again? At the current liquidity, this will be impossible no matter the virality score; even if the virality score would rise further, the sell price would be stuck in the second case where it is independent of the virality score. If the virality score were to fall enough for the sell price to be calculated according to the first case, then this would only imply a sell price even lower than 8.02. To break even or to make a profit, user B is thus dependent on liquidity increasing. Concretely, assuming that virality stays at 1,000, the current liquidity of 37,583.51 would have to rise to 465,452.93 for user B to not sell at a loss. The multiplicative increase in liquidity required is thus around 12.38. It would not help user B much if the virality score would increase further. Assuming the virality score increases tenfold another time to 10,000, then liquidity would have to rise to 385,245.39 for user B to not sell at a loss. The multiplicative increase in liquidity required here is around 10.25. In fact, if , then buys will never be able to bring to , so sell price in the considered scenario where virality increases further after user B's buy will always be calculated according to the second case. Let be the ratio of user B's buy price to user B's sell price if immediately selling again, which is about . Then we will show in the next subsection that user B will only be able to break even if liquidity increased by a factor of at least .
To calculate the concrete values mentioned above, we wrote a script reproduced in the last two subsections of this section.
Bounding liquidity required to break even
We assume that the initial supply was , the supply is now , and user B bought the token that raised the supply to . The virality score is , liquidity at this point is , and is as defined before. We consider the situation where . Let be the ratio between user B's buy price and the price at which they could immediately sell again. Concretely, is the following.
Now suppose that further buys happened, bringing supply to and liquidity to , with corresponding new . We do not assume that the virality score stayed the same. If B sells their token now, their sell price will satisfy the following.
User B's buy price was instead
For user B to break even, we must have . We are interested in the minimum liquidity at which this is possible. We obtain
Note that must be strictly bigger than , as would imply no buys happened, and hence , and then the above inequality would contradict . We continue from above:
What we want to bound below is exactly . We thus obtain the following chain of inequalities. We use that , and that implies and hence .
Now as as we remarked above, and both are natural numbers, we must have , and hence we can conclude . Thus, we can conclude
So if user B bought the token in a situation where the buy price was times the sell price, then user B will only be able to break even on selling this token when the liquidity multiplied by a factor of at least . We emphasize that this holds no matter how the virality score changes after user B bought their token.
Script to calculate example values
The following SageMath script was used to calculate the values for the concrete example given above.
#!/usr/bin/env sage
import random
INITIAL_SUPPLY = 10_000
INITIAL_VIRALITY = 100
SUPPLY = 20_000
VIRALITY = 1000
VIRALITY_LAST = 10000
PRICE_CONSTANT = 10**(-14)
def sum_of_cubes(x):
"1^3 + ... + x^3"
return ((x*(x+1))/2)^2
def test_sum_of_cubes():
x = random.randrange(100, 1000)
correct = sum([i**3 for i in range(x+1)])
assert sum_of_cubes(x) == correct
test_sum_of_cubes()
def getPrice(supply, score):
return float(PRICE_CONSTANT * score * (supply**3))
def sellPrice(supply, score, liquidity):
price_from_buy = getPrice(supply, score)
price_from_liquidity = (liquidity * (supply**3)) / (sum_of_cubes(supply) - sum_of_cubes(INITIAL_SUPPLY))
return min([price_from_buy, price_from_liquidity])
def liquidity_after_buys(supply_before, supply_after, score, liquidity):
supply = supply_before
while supply < supply_after:
supply += 1
liquidity += getPrice(supply, score)
return liquidity
def solve_supply(sell_price, current_supply, current_liquidity, score):
S = var('S')
# We want
# (liquidity * (S**3)) / (sum_of_cubes(S) - sum_of_cubes(INITIAL_SUPPLY)) >= sell_price
# So look for S with
# (liquidity * (S**3)) = sell_price * (sum_of_cubes(S) - sum_of_cubes(INITIAL_SUPPLY))
# Note liquidity also depends on S.
liquidity = current_liquidity + PRICE_CONSTANT * score * (sum_of_cubes(S) - sum_of_cubes(current_supply))
lhs = liquidity * (S**3)
rhs = sell_price * (sum_of_cubes(S) - sum_of_cubes(INITIAL_SUPPLY))
S_by_solving = (lhs == rhs).find_root(0, 10**6)
S_by_solving = ceil(S_by_solving)
#S = current_supply + 1
#while sellPrice(S, score, liquidity_after_buys(current_supply, S, score, current_liquidity)) < sell_price:
# S += 1
#S_by_trying = S
#assert S_by_solving == S_by_trying
return S_by_solving
liquidity = liquidity_after_buys(INITIAL_SUPPLY, SUPPLY, INITIAL_VIRALITY, 0)
print(f'Virality so far was {INITIAL_VIRALITY}')
print(f'Supply is {SUPPLY:,}, and liquidity {liquidity:,.2f}')
user_A_buy_price = getPrice(SUPPLY, INITIAL_VIRALITY)
print(f'User A bought the last token, for {user_A_buy_price:.2f}')
print(f'\nNow virality is {VIRALITY:,}.')
print(f'If user A were to sell now, they would get {sellPrice(SUPPLY, VIRALITY, liquidity):.2f}. Thus they would not make a profit yet, even though virality multiplied by 10.')
print(f'To obtain a sale price n times the price they bought at, user A would have to wait until liquidity increased by a factor of this:')
for n in range(2, 11):
S = solve_supply(n*user_A_buy_price, SUPPLY, liquidity, VIRALITY)
L = liquidity_after_buys(SUPPLY, S, VIRALITY, liquidity)
print(f'{n}: {L / liquidity:.2f}')
user_B_buy_price = getPrice(SUPPLY+1, VIRALITY)
liquidity += user_B_buy_price
print(f'\nIf user B buys a token now, it costs them {user_B_buy_price:.2f}')
user_B_sell_price = sellPrice(SUPPLY+1, VIRALITY, liquidity)
print(f'If they were to immediately sell again, they would get {user_B_sell_price:.2f}. Immediately after buying, their position is thus at a loss of {(1 - user_B_sell_price / user_B_buy_price)*100:.2f}%.')
user_B_breakeven_supply = solve_supply(user_B_buy_price, SUPPLY + 1, liquidity, VIRALITY)
user_B_breakeven_liquidity = liquidity_after_buys(SUPPLY + 1, user_B_breakeven_supply, VIRALITY, liquidity)
#print(sellPrice(user_B_breakeven_supply, VIRALITY, user_B_breakeven_liquidity))
print(f'Liquidity would have to rise from {liquidity:,.2f} to {user_B_breakeven_liquidity:,.2f} for user B to not sell at a loss. The multiplicative increase in liquidity required is thus {user_B_breakeven_liquidity / liquidity:.2f}.')
print(f"\nIt would not help user B's position much if virality would increase further.")
print(f'Assume virality would increase to {VIRALITY_LAST:,}.')
user_B_sell_price2 = sellPrice(SUPPLY+1, VIRALITY_LAST, liquidity)
print(f'If user B were to sell now, they would again only get {user_B_sell_price2:.2f}. Their position is thus still at a loss of {(1 - user_B_sell_price2 / user_B_buy_price)*100:.2f}%.')
user_B_breakeven_supply2 = solve_supply(user_B_buy_price, SUPPLY + 1, liquidity, VIRALITY_LAST)
user_B_breakeven_liquidity2 = liquidity_after_buys(SUPPLY + 1, user_B_breakeven_supply2, VIRALITY_LAST, liquidity)
print(f'Liquidity would have to rise from {liquidity:,.2f} to {user_B_breakeven_liquidity2:,.2f} for user B to not sell at a loss. The multiplicative increase in liquidity required is thus {user_B_breakeven_liquidity2 / liquidity:.2f}.')
Output of the script
The output of the SageMath script reproduced in the preceeding section is as follows.
Virality so far was 100
Supply is 20,000, and liquidity 37,503.50
User A bought the last token, for 8.00
Now virality is 1,000.
If user A were to sell now, they would get 8.00. Thus they would not make a profit yet, even though virality multiplied by 10.
To obtain a sale price n times the price they bought at, user A would have to wait until liquidity increased by a factor of this:
2: 2.06
3: 3.18
4: 4.35
5: 5.58
6: 6.85
7: 8.18
8: 9.54
9: 10.95
10: 12.41
If user B buys a token now, it costs them 80.01
If they were to immediately sell again, they would get 8.02. Immediately after buying, their position is thus at a loss of 89.98%.
Liquidity would have to rise from 37,583.51 to 465,452.93 for user B to not sell at a loss. The multiplicative increase in liquidity required is thus 12.38.
It would not help user B's position much if virality would increase further.
Assume virality would increase to 10,000.
If user B were to sell now, they would again only get 8.02. Their position is thus still at a loss of 89.98%.
Liquidity would have to rise from 37,583.51 to 385,245.39 for user B to not sell at a loss. The multiplicative increase in liquidity required is thus 10.25.
Recommendations
Consider using a different pricing model.