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”