This post is part of a series on DeFi. Here is the previous post, and this is the first post in the series.

Now we have a way to add liquidity to our Automated Market Maker (AMM), but why should people do that? The AMM needs liquidity to minimise slippage, but what’s in it for the liquidity providers (LPs)?

Putting assets into an AMM is risky, so LPs need an incentive in order to take that risk versus doing something else with their money. As usual in finance, whenever you want someone to take a risk, you have to pay them to do it.

Liquidity can be added/removed at any time, so before we discuss how to reward LPs, our AMM needs to be able to keep track of how much liquidity each of them provided.

Before we get into that, I want to quickly look again at removing liquidity.

Removing liquidity revisited

In the previous post, we implemented remove_liquidity in terms of removing a certain amount of ether, but that’s not going to work in practice, because liquidity consists of both ether and tokens.

When we add liquidity, we take care not to change the proportion of ether and tokens, using x/y = k. But, when counterparties trade on our AMM, the proportion of ether to tokens changes according to x * y = k. That means, if we only consider the ether when we remove liquidity after trading happens, we’re not going to get the right amounts.

Here’s an example:

$ bin/amm.rb
Amm eth: 0.0, tokens: 0.0, price: NaN eth/token

> zoe add 1 100
Amm eth: 1.0, tokens: 100.0, price: 0.01 eth/token

> bob buy 0.1
bob gets 9.090909090909093 tokens for 0.1 ether, price 0.0110
Amm eth: 1.1, tokens: 90.9090909090909, price: 0.012100000000000001 eth/token

> zoe remove 1
Amm eth: 0.10000000000000009, tokens: 8.264462809917362, price: 0.012100000000000001 eth/token

> counterparties
bob     ether: 9.9000,  tokens: 9.0909
zoe     ether: 10.0000, tokens: 982.6446
Amm eth: 0.10000000000000009, tokens: 8.264462809917362, price: 0.012100000000000001 eth/token


Although zoe provided all the liquidity to the AMM, when she removes 1 ETH, which is what she originally provided, there is still liquidity left in the pool in the form of 0.1 ETH and 8.26 tokens.

We need a better way to keep track of the liquidity of our AMM.

Liquidity Provider Tokens

We’re going to create a new value in our AMM called total_liquidity, representing all the liquidity it has.

class Amm
  attr_accessor :ether_reserve, :token_reserve, :total_liquidity

  def initialize(params = {})
    @ether_reserve = 0.0
    @token_reserve = 0.0
    @total_liquidity = 0.0
  end

  ...

When someone adds liquidity to the AMM, we will send back an amount of "liquidity provider tokens" (LP tokens) which reflects their additional share of the total_liquidity.

Let’s update our Counterparty class to keep track of how many LP tokens each counterparty has:

class Counterparty
  attr_accessor :name, :ether, :tokens, :lp_tokens

  def initialize(params)
    @name = params.fetch(:name)
    @ether = params.fetch(:ether, 0).to_f
    @tokens = params.fetch(:tokens, 0).to_f
    @lp_tokens = params.fetch(:lp_tokens, 0).to_f
  end
end

In the case of the first deposit of liquidity into the pool, this can just be the value of the ether they supplied (we could use the value of the tokens instead – it really doesn’t matter, as long as we’re consistent. I’m using ether because that’s what the uniswap v1 contract does).

If we’re adding to some existing liquidity, then we need to know what proportion of the total_liquidity the new ether represents.

Because we always keep the proportion of ether and tokens constant when we add/remove liquidity, we can just consider the proportion of ether and not worry about the tokens.

  def add_liquidity(counterparty, ether, max_tokens)
    tokens_added = 0.0
    liquidity_minted = 0.0

    if @total_liquidity > 0.0
      tokens_added = ether * (@token_reserve / @ether_reserve)
      liquidity_minted = ether * (@total_liquidity / @ether_reserve)
    else
      tokens_added = max_tokens
      liquidity_minted = ether
    end

    ...

Once we’ve figured out how much new liquidity we have, we just add it to the total_liquidity, and give the lp_tokens to the liquidity provider.

    if tokens_added > max_tokens
      log "Error: #{max_tokens} is not enough. #{tokens_added} required."
    else
      @token_reserve += tokens_added
      @ether_reserve += ether
      @total_liquidity += liquidity_minted

      counterparty.tokens -= tokens_added
      counterparty.ether -= ether
      counterparty.lp_tokens += liquidity_minted
    end
  end

Removing liquidity

Now that we are measuring liquidity using LP tokens to represent a share of the total liquidity in the AMM, we do the same calculation in reverse to remove liquidity:

  def remove_liquidity(counterparty, lp_tokens)
    ether_removed = lp_tokens * (@ether_reserve / @total_liquidity)
    tokens_removed = lp_tokens * (@token_reserve / @total_liquidity)

    if ether_removed > @ether_reserve
      log "Error: insufficient liquidity"
    else
      @token_reserve -= tokens_removed
      @ether_reserve -= ether_removed
      @total_liquidity -= lp_tokens

      counterparty.tokens += tokens_removed
      counterparty.ether += ether_removed
      counterparty.lp_tokens -= lp_tokens
    end
  end

Let’s see how this works in practice, with the same set of operations as before:

$ bin/amm.rb
Amm eth: 0.0, tokens: 0.0, price: NaN eth/token

> zoe add 1 100
Amm eth: 1.0, tokens: 100.0, price: 0.01 eth/token

> counterparties
bob     ether: 10.0000, tokens: 0.0000, lp_tokens: 0.0000
zoe     ether: 9.0000,  tokens: 900.0000, lp_tokens: 1.0000
Amm eth: 1.0, tokens: 100.0, price: 0.01 eth/token

> bob buy 0.1
bob gets 9.090909090909093 tokens for 0.1 ether, price 0.0110
Amm eth: 1.1, tokens: 90.9090909090909, price: 0.012100000000000001 eth/token

> zoe remove 1
Amm eth: 0.0, tokens: 0.0, price: NaN eth/token

> counterparties
bob     ether: 9.9000,  tokens: 9.0909, lp_tokens: 0.0000
zoe     ether: 10.1000, tokens: 990.9091, lp_tokens: 0.0000
Amm eth: 0.0, tokens: 0.0, price: NaN eth/token

zoe gets 1 LP token when she adds liquidity to the AMM.

After bob’s trade, zoe removes the 1 LP token, which represents 100% of the AMM’s liquidity, and this time the AMM is completely empty.

Now, regardless of how many people add/remove liquidity, and how many counterparties trade tokens back and forth, we have a way to keep track of which LPs provided liquidity to our AMM, and how much of the total liquidity each of them supplied.

You can see the full code here.

In the next post, we’ll use this information to reward LPs so that they have a reason to provide liquidity to the AMM.

2 thoughts on “Tracking Liquidity in the Automated Market Maker

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s