← Back to list

Liquidity Tree Performance using Stablecoins: Part 1

We show how Liquidity Trees improve market efficiency when compared to single pool AMMs

Ian Moore, PhD in DataDrivenInvestor · 2024-09-16 11:01 · 51 claps · 9.5 min read
#defi #liquidity-trees #simulation
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3 ECO · Economy · General

Liquidity Tree Performance using Stablecoins: Part 1

  • We show how Liquidity Trees improve market efficiency when compared to single pool AMMs
  • Apply stablecoins to control for impermanent loss
  • Utilizes the UniswapPy python package for analysis

1. Introduction

In our last article, we introduced a new class of DeFi primitives which we have defined as Liquidity Trees. These primitives serve as the core feature behind the soon-to-be released Pachira token supported by SYS Labs. In this article, we dive deeper into how Liquidity Trees improve performance.

Since there are some subtle nuances to consider when thinking about Liquidity Trees, for this discussion we will relaxing some of the constraints. This is so we can clearly demonstrate the underpinning principles on how liquidity trees work and how they generate returns. Thus, we will be: (a) using a Simple Tree as discussed in our previous article; (b) using stablecoins (ie, USDC and USDT) to control for impermanent loss; and (c) assuming an infinite supply of index tokens.

From an educational standpoint, we do this so to not obfuscate these Liquidity Tree principles when communicating to others. Hence, as we step through these topics, we will be layering in these considerations in subsequent articles, as we have found the study of these new class of DeFi primitives to be very rich (haha … pun intended).

To begin, in Fig 1 we outline the three main paths that one can consider when investing into a Liquidity Tree; which are as follows:

  • Path #1: single-sided deposit into parent pool and mint parent LP tokens (ie, the standard way of investing into a single AMM pool)
  • Path #2: single-sided deposit into child pool and mint child LP tokens
  • Path #3: single-sided deposit into parent pool and mint index tokens (ie, what we call a SwapIndexMint); next, perform a single-sided deposit of index tokens into child pool and mint tree LP tokens

Fig. 1 Investment paths into a Simple Liquidity Tree provides the option to mint parent, child or tree tokens; a Simple Tree is the simplest possible Liquidity Tree configuration

Fig. 1 Investment paths into a Simple Liquidity Tree provides the option to mint parent, child or tree tokens; a Simple Tree is the simplest possible Liquidity Tree configuration

The third path is the most interesting, as our investment is now making a return from both the parent and child liquidity pool (LP), thus generating higher yield. We are effectively investing into an LP, taking a fully collateralized loan from that investment and reinvesting it. Therefore earning yield from both our original investment and the fully collateralized loan. For the intuition on how tree tokens generate revenue and improve market efficiency, see Fig 2.

Fig 2. Boxes represent liquidity under constant product trading (CPT) curve; creating new market out of index liquidity is a way to address the stagnant liquidity problem; in short, we’ve leveraged some of the green, got red and made some extra purple

Fig 2. Boxes represent liquidity under constant product trading (CPT) curve; creating new market out of index liquidity is a way to address the stagnant liquidity problem; in short, we’ve leveraged some of the green, got red and made some extra purple

The leveraging principle behind tree tokens was discussed in our last article, however in this article we will be carefully demonstrating this via simulation. What’s interesting, is that Path #2 also shows an improvement in returns over the standard (ie, Path #1) as well, as our simulations will show. This has to do with how index tokens respond to price changes in the parent, as we will also discuss.

2. Simulate Asset Prices

First, we import our packages and simulate the market price of the USDC/USDT trading pair using a gamma distribution:

import scipy.stats as stats 
import statsmodels.api as sm
from uniswappy import *

# *************************
# *** Simulation
# *************************
n_sim_runs = 2000
shape = 2000
scale = 0.0005

p_arr = np.random.gamma(shape = shape, scale = scale, size = n_sim_runs)

3. Simulate Simple Tree

Next, we setup our Simple Tree using Uniswap V2. The following list highlights the key components of the UniswapPy python package that was used:

  • **MockAddress : **Creates mock ETH address
  • **ERC20: **ERC20 token
  • **UniswapExchangeData : **Uniswap V2/V3 exchange data class required for UniswapFactory instantiation
  • **UniswapFactory: **Uniswap V2/V3 LP factory for given token pairs
  • **Join: **Process to join x and y amounts to Uniswap LP
  • **JoinTree: **Process to join liquidity tree child LP with x token amount
  • **IndexVault: **Vault of index tokens
  • **IndexERC20: **Index ERC20 token (eg, iUSDC); see Uniswap Indexing Problem

The procedure for a Simple Tree setup is as follows:

user_nm = MockAddress().apply()
tkn_amount = 100000 
usdt_amount = p_arr[0]*tkn_amount 

tkn1 = ERC20(tkn_nm, "0x09")
usdt1 = ERC20(usdt_nm, "0x111")
exchg_data = UniswapExchangeData(tkn0 = tkn1, tkn1 = usdt1, symbol="LP", address="0x011")

iVault1 = IndexVault('iVault1', "0x7")

# Setup parent LP
factory = UniswapFactory(f"{tkn_nm} pool factory", "0x2")
lp = factory.deploy(exchg_data)
Join().apply(lp, user_nm, tkn_amount, usdt_amount)

# Setup child LP
tkn2 = ERC20(tkn_nm, "0x09")
itkn1 = IndexERC20(itkn_nm, "0x09", tkn1, lp)
exchg_data1 = UniswapExchangeData(tkn0 = tkn2, tkn1 = itkn1, symbol="LP1", address="0x012")
lp1 = factory.deploy(exchg_data1)
JoinTree().apply(lp1, user_nm, iVault1, 10000)

# Re-balance LP price after JoinTree
SwapDeposit().apply(lp, usdt1, user_nm, lp.reserve0-lp.reserve1)

lp.summary()
lp1.summary()
# OUTPUT
Exchange USDC-USDT (LP)
Reserves: USDC = 109999.99999999997, USDT = 109999.99999999997
Liquidity: 109983.02904170564 

Exchange USDC-iUSDC (LP1)
Reserves: USDC = 9972.071706380624, iUSDC = 4824.313840861382
Liquidity: 6936.022170895521 

If we look at the output of the child LP above, it’s worth noting the reserve amounts of USDC compared to iUSDC. Without knowing how liquidity trees are implemented, one would logically assume that these reserve amounts should be roughly identical. However, this is not to ever be the case, because the token amount passed into the child are always denominated in terms of parent LP tokens, which is an internal feature that gets carried all throughout the tree. However, the amount presented to the user through the protocol is continually rebased in terms of USDC.

Next, we take an investment position into our Simple Tree; the following list highlights the key components of the UniswapPy python package that are used:

  • **SwapIndexMint : **Process to swap-deposit single token as a single sided deposit into LP and mint its respective indexing token
  • **SwapDeposit: **Process to swap exact token x for token y, or vice versa; otherwise known as a single-sided deposit

We initialize the three investment paths outlined in Fig 1 via the following:

tkn_invest = 100
invested_user_nm = 'invested_user'

SwapIndexMint(iVault1, opposing = False).apply(lp, tkn1, invested_user_nm, tkn_invest)
mint_itkn1_deposit = iVault1.index_tokens[itkn_nm]['last_lp_deposit']
SwapDeposit().apply(lp1, itkn1, invested_user_nm, mint_itkn1_deposit)

lp.summary()
lp1.summary()

lp_invest_track  = lp.liquidity_providers[invested_user_nm]
lp1_invest_track  = lp1.liquidity_providers[invested_user_nm]

tkn_redeem_parent = RebaseIndexToken().apply(lp, tkn1, lp_invest_track)
itkn_redeem_child = RebaseIndexToken().apply(lp1, itkn1, lp1_invest_track)
tkn_redeem_tree = RebaseIndexToken().apply(lp, tkn1, itkn_redeem_child) 

print(f'{tkn_redeem_parent:.3f} USDC redeemed from {lp_invest_track:.3f} LP tokens if {tkn_invest:.1f} invested USDC immediately pulled from parent')
print(f'{tkn_redeem_child1:.3f} USDC redeemed from {lp1_invest_track:.3f} LP1 tokens if {tkn_invest:.1f} invested USDC immediately pulled from tree')
# OUTPUT
Exchange USDC-USDT (LP)
Reserves: USDC = 110099.99999999997, USDT = 109999.99999999997
Liquidity: 110032.93488691782 

Exchange USDC-iUSDC (LP1)
Reserves: USDC = 9972.071706380624, iUSDC = 4874.219686073561
Liquidity: 6971.751479075943 

99.700 USDC redeemed from 49.906 LP tokens if 100.0 invested USDC immediately pulled from parent
99.403 USDC redeemed from 35.729 LP1 tokens if 100.0 invested USDC immediately pulled from tree

We can see from the output above, that if we immediately redeem our tokens (after investing), we experience a small loss, which is attributed to the LP swap fees.

Finally, we simulate our Simple Tree; the following list highlights the key components of the UniswapPy python package that was used:

  • **CorrectReserves : **Applies SolveDeltas class to correct x/y reserve amounts so that LP price reflects the input market price (see my previous medium article for math)
  • **TokenDeltaModel: **Model for generating token amounts for trading, which are gamma distributed by default
  • **SettlementLPToken: **Determine settlement amount of LP token given a certain amount of token
  • **RebaseIndexToken: **Determine amount of token given a certain amount of liquidity from LP (ie, inverse of SettlementLPToken)
  • **Swap: **Process to swap exact token x for token y, or vice versa

The simulation comprises of a loop which steps through our market events where a number of actions are taken. These actions include: (a) using CorrectReserves to re-calibrate pool reserves to reflect market price, thus simulating arbitrage; (b) random swapping in both parent and child pools with conservatively less trading in the child pool; and (c) investment performance data capture of the three investment paths outlined in Fig 1.

arb = CorrectReserves(lp, x0 = 1)
arb1 = CorrectReserves(lp1, x0 = lp1.reserve1/lp1.reserve0)

TKN_amt = TokenDeltaModel(1000)

lp_parent_invest_arr = []; # Investment Path 1
lp1_child_invest_arr = []; # Investment Path 2
lp1_tree_invest_arr = [];  # Investment Path 3

for k in range(n_sim_runs):

    # *****************************
    # ***** Parent Arbitrage ******
    # *****************************   
    arb.apply(p_arr[k])

    # *****************************
    # ***** Child Arbitrage ******
    # *****************************       
    p_lp1 = SettlementLPToken().apply(lp, tkn1, lp1.reserve0)/lp1.reserve0
    arb1.apply(p_lp1)

    # *****************************
    # ***** Random Swapping ******
    # *****************************       
    Swap().apply(lp, tkn1, user_nm, TKN_amt.delta()) 
    Swap().apply(lp, usdt1, user_nm, TKN_amt.delta()) 

    # conservatively assume 20% of parent trading by volume
    Swap().apply(lp1, tkn2, user_nm, 0.2*TKN_amt.delta()) 
    Swap().apply(lp1, itkn1, user_nm, SettlementLPToken().apply(lp, tkn1, 0.2*TKN_amt.delta()))

    # *****************************
    # ******* Data Capture ********
    # *****************************

    # investment performance
    tkn_redeem_parent = RebaseIndexToken().apply(lp, tkn1, lp_invest_track)
    itkn_redeem_child = RebaseIndexToken().apply(lp1, itkn1, lp1_invest_track)    
    tkn_redeem_tree = RebaseIndexToken().apply(lp, tkn1, itkn_redeem_child) 

    lp_parent_invest_arr.append(tkn_redeem_parent)
    lp1_child_invest_arr.append(RebaseIndexToken().apply(lp1, tkn2, lp1_invest_track))    
    lp1_tree_invest_arr.append(tkn_redeem_tree)

4. Review Performance Output

In Fig 3, we plot the price responses of both the parent (USDC/USDT) and child (USDC/iUSDC) LPs that our simulation produces. Its interesting to note that we can visually see the higher volatility in the USDC/iUSDC price response.

Fig 3. One-year of simulated market prices, USDC/USDT parent LP price and USDC/iUSDC child LP price

Fig 3. One-year of simulated market prices, USDC/USDT parent LP price and USDC/iUSDC child LP price

Next, we compare the raw performance of the parent token vs the tree token:

lp.summary()
lp1.summary()

# Redeem investment from parent
tkn_redeem_parent = RebaseIndexToken().apply(lp, tkn1, lp_invest_track)

# Redeem investment from tree (child + parent)
itkn_redeem_child = RebaseIndexToken().apply(lp1, itkn1, lp1_invest_track)
tkn_redeem_tree = RebaseIndexToken().apply(lp, tkn1, itkn_redeem_child) 

print(f'{tkn_redeem_parent:.3f} USDC redeemed from {lp_invest_track:.3f} LP tokens if {tkn_invest:.1f} invested USDC pulled from parent')
print(f'{tkn_redeem_tree:.3f} USDC redeemed from {lp1_invest_track:.3f} LP1 tokens if {tkn_invest:.1f} invested USDC pulled from tree')
# OUTPUT
Exchange USDC-USDT (LP)
Reserves: USDC = 145244.3610640187, USDT = 144397.02963535133
Liquidity: 136868.88539213932 

Exchange USDC-iUSDC (LP1)
Reserves: USDC = 11552.506624583597, iUSDC = 5759.570930165687
Liquidity: 7673.167998716038 

105.741 USDC redeemed from 49.906 LP tokens if 100.0 invested USDC pulled from parent (lp)
113.214 USDC redeemed from 35.729 LP1 tokens if 100.0 invested USDC pulled from tree (lp + lp1)

After our one-year simulation, we can see that our invested ~ 50 LP parent tokens are now worth ~105.7 USDC compared to our invested ~36 LP1 tree tokens which are now worth ~113.2 USDC. This equates to a ~130% increase in performance!

Due to the volatile nature of performance returns, we applied Lowess smoothing to the token performance time series to get a clearer picture for comparison. You’ll see in our next article when we factor in a finite supply of index tokens (using a finite state machine), these sharp idiosyncrasies get corrected.

# Smooth returns
lowess = sm.nonparametric.lowess
x = range(0,n_sim_runs)
res = lowess(lp_parent_invest_arr, x, frac=1/15); sm_lp_parent = res[:,1]
res = lowess(lp1_child_invest_arr, x, frac=1/15); sm_lp1_child = res[:,1]
res = lowess(lp1_tree_invest_arr, x, frac=1/15); sm_lp1_tree= res[:,1]

print(f'{tkn_invest:.3f} TKN before is worth {sm_lp_parent[-1]:.3f} TKN after direct investment into parent (lp)') 
print(f'{tkn_invest:.3f} TKN before is worth {sm_lp1_child[-1]:.3f} TKN after direct investment into child (lp1)') 
print(f'{tkn_invest:.3f} TKN before is worth {sm_lp1_tree[-1]:.3f} TKN after investment into simple tree (lp+lp1)')
# Output
100.000 TKN before is worth 105.418 TKN after direct investment into parent (lp)
100.000 TKN before is worth 107.874 TKN after direct investment into child (lp1)
100.000 TKN before is worth 112.060 TKN after investment into simple tree (lp + lp1)

When looking at expected returns, we can see that the child token investment equates to a ~45% improvement over the parent, and the tree token investment equates to a ~123% improvement over the parent. It is important to note that these are the results of one tree configuration. We have found that results are largely dependant on the relative amount of trading done through the children over the parent. However, as indicated in the code block above, we have conservatively assumed only 20% of parent trading by volume through the child.

Fig 4. Simple tree performances of the parent, child and tree tokens outlined via the three select investment paths using conservative assumptions

Fig 4. Simple tree performances of the parent, child and tree tokens outlined via the three select investment paths using conservative assumptions

Finally, we look at the kernel density estimates of the price response of USDC/USDT in the parent compared to USDC/iUSDC in the child in Fig 5. If we look at the longer tails in the distribution of the USDC/iUSDC price response, its clearly evident that the child is more volatile than the parent. Isn’t that the case with children in general (joke)?

Fig 5. Distributions of USDC/USDT parent LP price and USDC/iUSDC child LP price; we can clearly see that the USDC/iUSDC prices are more volatile, which helps explain why the child pool consistently generates a higher return

Fig 5. Distributions of USDC/USDT parent LP price and USDC/iUSDC child LP price; we can clearly see that the USDC/iUSDC prices are more volatile, which helps explain why the child pool consistently generates a higher return

As discussed in my previous article, this has to do with how the price of index tokens in the child LPs respond to the price of the native tokens within the parent LP. When the reserve balances in the parent changes, this effect has an immediate impact on the price in the child due to the immediate change in supply of the index token (ie, iUSDC). This is where nuance (mentioned above) and the beauty of liquidity trees comes in. However, when we implement a finite state machine into the simulation (as we will be discussing in my next article), we will see that this volatility does calm down, but is still higher than the parent.

5. Summary

In summary, we have taken a deep dive into a new class of DeFi primitives called Liquidity Trees, and we use stablecoins to control for impermanent loss. For simplicity’s sake, we also assumed an infinite supply of index tokens. However, since index tokens are native to our tree, this limited supply needs to be factored in. To account for this, we use a finite state machine which we will integrate into the simulation for our next article.

We have found that these improvements, are invariant to the simulation settings. Hence, liquidity tree tokens consistently outperform the parent tokens. To better understand or to try other configurations, you can find the script to this presentation on the defipy-devs repos. If you want to stay up-to-date with this exciting project, you can visit the Pachira project website, or follow me on Twitter/X!

TextBook

If you enjoy my DeFi analytics content, you’ll love the official textbook:

📘 DeFiPy: Python SDK for On-Chain Analytics AMM math • Uniswap V2/V3 • Balancer • Stableswap • liquidity modeling • agents 👉 https://www.amazon.com/dp/B0G3RV5QRB

Visit us at *DataDrivenInvestor.com*

Subscribe to DDIntel *here*.

Join our creator ecosystem *here*.

DDI Official Telegram Channel: https://t.me/+tafUp6ecEys4YjQ1

Follow us on *LinkedIn, [Twitter](https://twitter.com/@DDInvestorHQ), [YouTube](https://www.youtube.com/c/datadriveninvestor), and [Facebook](https://www.facebook.com/datadriveninvestor)*.


메타데이터
post_id
ee09eae2fb86
slug
liquidity-tree-performance-using-stablecoins-part-1-ee09eae2fb86
url
https://medium.datadriveninvestor.com/liquidity-tree-performance-using-stablecoins-part-1-ee09eae2fb86
canonical_url
https://medium.datadriveninvestor.com/liquidity-tree-performance-using-stablecoins-part-1-ee09eae2fb86
author_url
https://medium.com/@icmoore
status
ok
fetched_at
2026-06-14 11:28:49