Wasted Liqudity

Liquidity-fragmentation-is-bad team here, coming at you with a detailed write up about liquidity fragmentation!

The basics:

What is liquidity fragmentation?

Liquidity fragmentation, in general, is the unavoidable fact that assets trade on many venues and liquidity of the asset must be split between these venues. For example, eth trades on both Coinbase and Binance and the prices may differ slightly between these exchanges. Another example would be Coinbase and Balancer.

Note that this same definition can also apply within Balancer itself. Eth may trade via many different pools such as ETH/BTC or ETH/USDC. This is still normal, inevitable and unavoidable liquidity fragmentation, but the next step is actual wasted liquidity, so…

What is wasted liquidity?

Wasted liquidity is when we go one-lever deeper: on Balancer today eth doesn’t just trade on one ETH/BTC pools. It trades on two. And unlike the ETH/USDC pool – that, remember, is good – this second ETH/BTC pool is bad.

It’s an exact clone of the other pool. No, really, check it:

Wasted $445702 on [{'addr': '0x29...a177', 'name': ['WBTC50', 'WETH50'], 'Fee': 0.15}, {'addr': '0x80...2e9d', 'name': ['WBTC50', 'WETH50'], 'Fee': 0.15}]

Click through and admire the sameness:
https://pools.balancer.exchange/#/pool/0x294de1cde8b04bf6d25f98f1d547052f8080a177
https://pools.balancer.exchange/#/pool/0x80b42669f3d8bba7618800efe6aed60d90f72e9d

That is wasted liquidity and it is one of many examples on Balancer today.

What can we do about it?

Two things:

  1. If you have liquidity in a wasteful pool, move it to its larger cousin. Only by working together can we defeat wasted liquidity.

  2. Penalize liquidity in wasteful pools: give them fewer BAL rewards.

What is the catch?

Two things:

1) Wasted liquidity isn’t entirely objective.

Some pools are like those two BTC/ETH pools provided as an example above, but others are probably wasteful, but require a judgement call. Give it a try, does this look like a fair group to you?

{'addr': '0xa2...bd95', 'name': ['WBTC49', 'cETH49', 'COMP2'], 'Fee': 0.15},
{'addr': '0x7a...ae25', 'name': ['WBTC49', 'cETH49', 'COMP2'], 'Fee': 0.15},
{'addr': '0xab...3b3c', 'name': ['WBTC48', 'cETH50', 'COMP2'], 'Fee': 0.15},
{'addr': '0xc7...1803', 'name': ['WBTC49', 'cETH49', 'COMP2'], 'Fee': 0.15},
{'addr': '0x7c...cb71', 'name': ['WBTC50', 'cETH48', 'COMP2'], 'Fee': 0.15}

Those’re some awfully similar pools, but they’re not identical. Should they be penalized as one group?

2) Naive penalties are gameable.

It’s difficult to tell which pool, out of a group of similar pools, should be spared from penalty. Clearly one of them, but how to tell? We have two reasonable options:

  1. The oldest.

  2. The largest.

The drawback of choosing the oldest is precisely that it might not also be the largest. If this turned out to be so, we’d then be demanding that more liquidity move than was necessary, which is inefficient.

Choosing the largest means our choice may move around in gameable ways: what if many minnows are supplying a healthy ETH/USDC pool and a spiteful whale swoops in and creates a bigger ETH/USDC pool. Our minnows migrate to the new pool, but the whale repeats the ploy!

Those both seem kind of bad?

Yeah. Here’s our best path forward:

In the short-term, we go with the oldest pool. Or even just the “most popular right now” pool. The point is, we pick a winning pool out of each group and penalize the rest. New pools that would get grouped with one of these “winners” are penalized just like the existing ones.

In the longer-term, we commit to eventually deprecate our “winners” system and allow the largest pool in a given group to be the one to go unpenalized. The assumption here would be that eventually Balancer would exit growth mode and could charge a small (but large enough to deter griefing) BAL cost on the creation of new pools.

The Code:

Change FEE_DIFF and ALC_DIFF to match what you think they ought to be.

FEE_DIFF is how different two fees must be to no longer consider two pools “the same”. 1 is a 0.01% difference: e.g. a 0.25% fee pool and a 0.30% fee pool are similar if FEE_DIFF >= 5.

ALC_DIFF is how different two matching asset allocations must be, +/- from each other, to no longer consider two pools “the same”. 1 is a 1% difference: e.g. BAL/ETH 80/20 and BAL/ETH 75/25 are similar if ALC_DIFF >= 5.

from bs4 import BeautifulSoup as bs
from requests import get

FEE_DIFF = 5
ALC_DIFF = 5

def parse(s):
    return [{'asset': i.lstrip()[0:i.lstrip().find(' ')], 'percent': int(i[i.lstrip().find(' ')+1:])} for i in s.split('%')[:-1]]

def similar(a, b):
    if abs(a['Fee'] - b['Fee'])*100 > FEE_DIFF:
        return False
    a, b = a['Assets'], b['Assets']
    return False not in [True in [i['asset'] in [j['asset'] for j in b] and abs(j['percent'] - i['percent']) <= ALC_DIFF for j in b if j['asset'] == i['asset']] for i in a]

data = []
for i in bs(get("http://www.predictions.exchange/balancer/None").text, features="lxml").find("table", id="balTable").find_all("tr"):
    try:
        addr, assets, fee, liq, _, _, _, _, _, _ = [j.text.strip() for j in i.find_all('td')]
        data.append({'Address': addr, 'Assets': parse(assets), 'Liquidity': int(liq.lstrip('$').replace(',', '')), 'Fee': float(fee.rstrip('%'))})
    except:
        pass

total_wasted = 0
counted = []
for i in data:
    grouped = [a for a in data if similar(a, i)]
    if len([j for j in counted if j in grouped]) > 0:
        continue
    liq = [a['Liquidity'] for a in grouped]
    ml = max(liq)
    tl = sum(liq)
    wasted = tl - ml
    total_wasted += wasted
    if wasted > 1:
        print('Wasted $%i on %s' % (wasted, [{'addr': i['Address'], 'name': ['%s%i' % (j['asset'], j['percent']) for j in i['Assets']], 'Fee': i['Fee']} for i in grouped]))
    counted.append(i)
print('Wasted $%i total' % total_wasted)

Here’s a recent snapshot with the default “5” DIFF values:

Wasted $445702 on [{'addr': '0x29...a177', 'name': ['WBTC50', 'WETH50'], 'Fee': 0.15}, {'addr': '0x80...2e9d', 'name': ['WBTC50', 'WETH50'], 'Fee': 0.15}]
Wasted $87678 on [{'addr': '0x5e...07d5', 'name': ['sETH50', 'WETH50'], 'Fee': 0.01}, {'addr': '0x61...a095', 'name': ['sETH55', 'WETH45'], 'Fee': 0.02}]
Wasted $9971 on [{'addr': '0x57...0fbe', 'name': ['DAI50', 'USDC50'], 'Fee': 0.05}, {'addr': '0x2b...2b4a', 'name': ['DAI50', 'USDC50'], 'Fee': 0.05}]
Wasted $7102199 on [{'addr': '0xa2...bd95', 'name': ['WBTC49', 'cETH49', 'COMP2'], 'Fee': 0.15}, {'addr': '0x7a...ae25', 'name': ['WBTC49', 'cETH49', 'COMP2'], 'Fee': 0.15}, {'addr': '0xab...3b3c', 'name': ['WBTC48', 'cETH50', 'COMP2'], 'Fee': 0.15}, {'addr': '0xc7...1803', 'name': ['WBTC49', 'cETH49', 'COMP2'], 'Fee': 0.15}, {'addr': '0x7c...cb71', 'name': ['WBTC50', 'cETH48', 'COMP2'], 'Fee': 0.15}]
Wasted $409419 on [{'addr': '0xca...c597', 'name': ['cUSDC50', 'cUSDT50'], 'Fee': 0.05}, {'addr': '0x01...6efd', 'name': ['cUSDC50', 'cUSDT50'], 'Fee': 0.05}, {'addr': '0xa1...a19e', 'name': ['cUSDC50', 'cUSDT50'], 'Fee': 0.01}]
Wasted $325043 on [{'addr': '0xc8...307d', 'name': ['PNK50', 'WETH50'], 'Fee': 0.1}, {'addr': '0x77...2148', 'name': ['PNK55', 'WETH45'], 'Fee': 0.15}]
Wasted $559616 on [{'addr': '0x94...3b79', 'name': ['cDAI50', 'cUSDT50'], 'Fee': 0.05}, {'addr': '0x4d...cc9f', 'name': ['cDAI50', 'cUSDT50'], 'Fee': 0.1}, {'addr': '0xb8...53ea', 'name': ['cDAI50', 'cUSDT50'], 'Fee': 0.01}]
Wasted $279894 on [{'addr': '0xcd...0ccd', 'name': ['TUSD50', 'USDC50'], 'Fee': 0.05}, {'addr': '0x62...3fe7', 'name': ['TUSD50', 'USDC50'], 'Fee': 0.09}]
Wasted $297578 on [{'addr': '0x59...0138', 'name': ['cUSDC50', 'cDAI50'], 'Fee': 0.05}, {'addr': '0x62...4a01', 'name': ['cUSDC50', 'cDAI50'], 'Fee': 0.05}]
Wasted $106921 on [{'addr': '0xc3...0e57', 'name': ['NMR80', 'WETH20'], 'Fee': 0.3}, {'addr': '0x87...c2e7', 'name': ['NMR75', 'WETH25'], 'Fee': 0.25}]
Wasted $9343 on [{'addr': '0x57...0a94', 'name': ['MKR50', 'WETH50'], 'Fee': 0.14}, {'addr': '0xd3...4afe', 'name': ['MKR50', 'WETH50'], 'Fee': 0.15}]
Wasted $97484 on [{'addr': '0x01...4c0d', 'name': ['GRID90', 'WETH10'], 'Fee': 0.25}, {'addr': '0x99...c061', 'name': ['GRID95', 'WETH5'], 'Fee': 0.2}]
Wasted $27127 on [{'addr': '0x80...fc36', 'name': ['aTUSD16', 'aSUSD16', 'aBUSD16', 'aUSDT16', 'LEND2', 'aUSDC16', 'aDAI16'], 'Fee': 0.0}, {'addr': '0x2f...3d7c', 'name': ['aTUSD16', 'aSUSD16', 'aBUSD16', 'aUSDT16', 'aUSDC16', 'aDAI16'], 'Fee': 0.01}]
Wasted $5391 on [{'addr': '0x82...8b34', 'name': ['WETH50', 'MFT50'], 'Fee': 0.1}, {'addr': '0x03...d490', 'name': ['WETH50', 'MFT50'], 'Fee': 0.1}]
Wasted $48758 on [{'addr': '0xe1...3221', 'name': ['TRB97', 'USDC2'], 'Fee': 0.09}, {'addr': '0xb1...81da', 'name': ['TRB97', 'USDC2'], 'Fee': 0.09}]
Wasted $9812124 total

Discuss:

  1. Other paths forward.
  2. Things I missed.
  3. What FEE_DIFF value would be appropriate?
  4. What ALC_DIFF value would be appropriate?
  5. What would be an appropriate factor penalty to apply to penalized pools?
4 Likes

Great proposal! I don’t have a better suggestion how to handle it.

My suggestions:
FEE_DIFF <= 4
ALC_DIFF <= 4
Penalty = 50% of the BAL reward

1 Like

Really great proposal. In order to be effective, probably it will need some UX changes in order to advise when you create a new pool that there are some similar ones and that your pool will be penalized.

Other important issue is for private pools, where probably they need to be private even there are other publics that are similar. That proposal will penalize private management.

3 Likes

Is the liquidity from multiple similar pools really wasted? Or does it just make the router less gas efficient?

2 Likes

Less gas efficient, the SOR will route through multiple pools if doing so presents the best price. Do consider that “less gass efficient”, in the current gas environment, frequently means the liquidity is effectively wasted.

One of the primary things Balancer competes with other on-chain AMMs about is gas efficiency.

3 Likes

Signed up precisely to comment on this proposal.
From a psychological point two many choices harm new users experience when choosing the right pool. Eventually the user might just leave for a competitor.