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

We want to be able to add more assets to our Automated Market Maker (AMM), to minimise slippage and to make it more attractive to traders (counterparties).

For now, we’re going to skip discussing why people would want to add assets to our AMM. We’ll cover that in a later post. For now, we’re just going to look at a mechanism to make it possible to add and remove assets.

The volume of assets (ether and tokens, in our case) in the AMM is known as "liquidity".

Constant price

When counterparties buy/sell tokens on our AMM, the token price is automatically adjusted based on the algorithm x * y = k.

When we add/remove liquidity, we need to change the quantities of both tokens and ether such that the price doesn’t change. So, for adding/removing liquidity, the algorithm is x/y = k.

Since the value of k (which we’ve called @konst in our code) will change whenever we add/remove liquidity, we can no longer calculate it once when we initialise our AMM. Instead, let’s calculate it when we’re trading:

  def trade(amount, input_reserve, output_reserve)
    konst = @token_reserve * @ether_reserve

    new_input_reserve = input_reserve + amount
    new_output_reserve = konst / new_input_reserve
    proceeds = output_reserve - new_output_reserve
    return [proceeds, new_input_reserve, new_output_reserve]
  end

add_liquidity

Let’s change our Amm class so it starts with no assets:

  def initialize
    @ether_reserve = 0.0
    @token_reserve = 0.0
  end

Now, we’ll implement a method to add liquidity to the AMM:

  def add_liquidity(counterparty, ether, max_tokens)

Liquidity will be added by a Counterparty, and they’ll add some ether and some tokens. Depending on the current ratio of ether to tokens, the amount of tokens they send as a parameter when they call add_liquidity might be more than the amount we need. That’s why we’re referring to it as max_tokens rather than tokens.

In the case of the first call to add_liquidity, all we need to do is store all the ether and all the tokens, and take them from the counterparty.

If we already have some assets in the AMM, then we will add however many tokens correspond to the ether value, at the current ratio of tokens to ether in the AMM, and we’ll take that many tokens from the counterparty:

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

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

    @token_reserve += tokens_added
    @ether_reserve += ether

    counterparty.tokens -= tokens_added
    counterparty.ether -= ether
  end

remove_liquidity

Removing liquidity is very similar, except we don’t need a token value from the counterparty – we’ll just remove however many tokens are appropriate, based on the amount of ether being removed. We also need to check that we have enough ether in the reserve:

  def remove_liquidity(counterparty, ether)
    tokens_removed = ether * (@token_reserve / @ether_reserve)

    if ether > @ether_reserve
      log "Error: insufficient liquidity"
    else
      @token_reserve -= tokens_removed
      @ether_reserve -= ether

      counterparty.tokens += tokens_removed
      counterparty.ether += ether
    end
  end

In practice, this is not how we would remove liquidity. We’ll discuss why in a subsequent post.

Adding/removing liquidity in action

To use these new features, we need to make some changes to our script:

  • We don’t specify any ether/tokens values when we create our Amm object
  • We need at least one counterparty, with both ether and tokens, to act as a liquidity provider
amm = Amm.new

alice = Counterparty.new(name: "alice", ether: 10)
bob = Counterparty.new(name: "bob", ether: 10)
zoe = Counterparty.new(name: "zoe", ether: 10, tokens: 1000)

counterparties = Counterparties.new([alice, bob, zoe])

We also need to tweak our case statement to add options for adding/removing liquidity:

I’m using "add" and "remove" as aliases for "add_liquidity" and "remove_liquidity", because I’m too lazy to type

  when /(.*) add_liquidity (.*) (.*)/, /(.*) add (.*) (.*)/
    counterparty = counterparties.find($1)
    amm.add_liquidity(counterparty, $2.to_f, $3.to_f)

  when /(.*) remove_liquidity (.*)/, /(.*) remove (.*)/
    counterparty = counterparties.find($1)
    amm.remove_liquidity(counterparty, $2.to_f)

You can see all the code, with some extra logging output, here.

Let’s see how this works (some lines omitted for clarity):

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

> c
zoe     ether: 10.0000, tokens: 1000.0000
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

> zoe add 1 900
Amm eth: 2.0, tokens: 200.0, price: 0.01 eth/token

> c
zoe     ether: 8.0000,  tokens: 800.0000
  1. zoe adds an initial 1 ether and 100 tokens. This sets the ratio of tokens to ether in the AMM
  2. zoe adds a further 1 ether, and offers 900 tokens, but the AMM only takes 100 tokens, so that the token price stays constant

Once zoe provides some liquidity, alice and bob can trade on the AMM as before.

Now we have a mechanism for adding more assets to our AMM to make it a better market for traders. In the next post, we’ll look at how to incentivise liquidity providers (like zoe) to put their assets into the AMM.

One thought on “Adding Liquidity to the AMM

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