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".
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
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
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
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.