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
- zoe adds an initial 1 ether and 100 tokens. This sets the ratio of tokens to ether in the AMM
- 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”