Blockchain
-
October 10, 2022

How to Create Your Own DeFi AMM (Easy guide!)

On Ethereum and other smart contract networks like BNB Smart Chain, interest in decentralized finance (DeFi) has skyrocketed.

Tokenized Bitcoin (BTC) is expanding on Ethereum, the popularity of "yield farming" to distribute tokens is rising, and the volume of "flash loans" is skyrocketing.

The number of people using automated market maker protocols like Uniswap is growing while the quantities traded on these platforms remain competitive.

But how do these transactions take place? How well do AMMs stack up against order book exchanges? We’ll explore that.

What is an AMM?

An AMM is a type of DEX (Decentralized Exchange) protocol that facilitates the elimination of order books in liquidity determination. Instead it uses an arithmetic algorithm to determine prices. To understand the role of AMMs let's see the following example:

Customer A has ETH and wants to buy DAI, in a centralized exchange there must be a customer B willing to sell DAI, and a system that matches buy orders with sell orders (using the exchange as an intermediary).

But what if customer B does not exist at that time? It will be almost impossible for A to complete this transaction. That's where markets require AMMs. They allow immediate liquidity to the necessary exchanges and set asset prices automatically by a predefined equation.

AMM Equation

For the purpose of this guide we will work with the equation X + Y = K known as "Constant Sum AMM" where X is the amount of one token and Y is the amount of the other. In this formula, K is a fixed constant.

There are other AMMs that make use of the equation X * Y = K, for example, Uniswap.

Uniswap Cryptocurrency
Uniswap Cryptocurrency

How to Create A Simple AMM

Let's see in code how they work and how to create a simple AMM:

Step 1:

AMMs work with exchange currency pairs e.g. USDC/DAI. Each DEX can be composed of several AMMs with different pairs. Let's create the initial structure of our contract:

1) First create the token instances that will be our exchange currency pairs (USDC/DAI for this example):


IERC20 public immutable token0;
IERC20 public immutable token1;

2) Then create variables to keep track of reserves


uint public reserve0;
uint public reserve1;

3) Create variables to keep track of our share tokens (more about this below).


uint public totalSupply;
mapping(address => uint) public balanceOf;

4) Create the constructor where we define the contract address of the exchange currency pair that our AMM will use.


constructor(address _token0, address _token1) {
  // NOTE: This contract assumes that token0 and token1
  // both have same decimals
  token0 = IERC20(_token0);
  token1 = IERC20(_token1);
}

5) Then write functions to make mint and burn from our shares tokens


function _mint(address _to, uint _amount) private {
  balanceOf[_to] += _amount;
  totalSupply += _amount;
}

function _burn(address _from, uint _amount) private {
  balanceOf[_from] -= _amount;
  totalSupply -= _amount;
}

6) Finally, write a function to update the values of our reserves.


function _update(uint _reserve0, uint _reserve1) private {
  reserve0 = _reserve0;
  reserve1 = _reserve1;
}

Step  2:

For an AMM to work there must be a person who injects liquidity to both pairs, in this case, who deposits USDC and DAI in the contract.

This actor is known as Liquidity Provider, they are in charge of injecting liquidity to a protocol in order to obtain future returns (The returns and strategies of the liquidity providers are not discussed in this article) Let's create the function "addLiquidity":

  1. First, we define our function, set as parameters the amount of token0 and token1 deposited by the liquidity provider and return the amount of shares that the AMM will assign to it.

function addLiquidity(uint _amount0, uint _amount1) external returns (uint shares) {}

                Now we will create the body of our function...

  1. Transfer funds from the liquidity provider to the AMM contract. Then input variables to keep track of our contract balances for each token.

  token0.transferFrom(msg.sender, address(this), _amount0);
  token1.transferFrom(msg.sender, address(this), _amount1);

  uint bal0 = token0.balanceOf(address(this));
  uint bal1 = token1.balanceOf(address(this));

  1. Keep track of the actual amount deposited by the liquidity provider.

  uint d0 = bal0 – reserve0;
  uint d1 = bal1 – reserve1;

  1. Shares are tokens that the AMM issues and allocates to liquidity providers to represent a participation in our liquidity pool (big pile of funds) and future returns.  In the contract if some shares have already been created, we calculate the shares to mint based on the total supply.
/* a = amount in L = total liquidity s = shares to mint T = total supply s should be proportional to increase from L to L + a (L + a) / L = (T + s) / T s = a * T / L */ if (totalSupply > 0) { shares = ((d0 + d1) * totalSupply) / (reserve0 + reserve1); } else { shares = d0 + d1; }
  1. Mint the shares and assign them to the liquidity provider, we must also update our reserves with the new balance sheet values.

require(shares > 0, “shares = 0”);
  _mint(msg.sender, shares);

  _update(bal0, bal1);


Step 3:

The main function of an AMM is to provide liquidity to exchanges. Unlike centralized exchanges, these exchanges are peer-to-peer (P2P), in this case there are no intermediaries and the exchanges are peer-to-contract.

Each time a swap is performed the protocol will take a certain percentage of the output token (0.3% for this example). Let's see our function to make a swap: 

  1. We define our function. We set as parameter the contract address of the token we want to exchange and its amount and  return the amount of the output tokens.

function swap(address _tokenIn, uint _amountIn) external returns (uint amountOut) {}

We create the body of our function...

  1. Validate that the tokens to be swapped are those supported by the AMM.

require(
  _tokenIn == address(token0) || _tokenIn == address(token1),
  “invalid token”
);

  1. Identify and keep track of which is the input token and which will be the output token. Identify the reserves we have for each one.

bool isToken0 = _tokenIn == address(token0);
  (IERC20 tokenIn, IERC20 tokenOut, uint resIn, uint resOut) = isToken0
    ? (token0, token1, reserve0, reserve1)
    : (token1, token0, reserve1, reserve0);

  1. Transfer the amount of the token to be swapped from the client to the AMM contract. We input a variable to keep track of the amount of tokens entered. 

tokenIn.transferFrom(msg.sender, address(this), _amountIn);
uint amountIn = tokenIn.balanceOf(address(this)) – resIn;

  1.  The AMM retains a 0.3% commission on the outgoing token.

amountOut = (amountIn * 997) / 1000;

  1. We calculate the new values of our reserves:

  (uint res0, uint res1) = isToken0
    ? (resIn + amountIn, resOut – amountOut)
    : (resOut – amountOut, resIn + amountIn);

  1. Update our reserves and make the swap effective, sending the outbound tokens to the customer.

  _update(res0, res1);
  tokenOut.transfer(msg.sender, amountOut);

Step 4:

It is important for liquidity providers to be able to withdraw the funds they originally deposited + some returns (in this article we will not take those returns into account). Let's code our "removeLiquidity" function: 

  1. Let's define our function. We set as parameter the amount of token shares and return the amount that was withdrawn from each token.


function removeLiquidity(uint _shares) external returns (uint d0, uint d1) {}

  1. We cCalculate the corresponding values to be withdrawn for each token.

  /*
    a = amount out
    L = total liquidity
    s = shares
    T = total supply

    a / L = s / T

    a = L * s / T
      = (reserve0 + reserve1) * s / T
  */
  d0 = (reserve0 * _shares) / totalSupply;
  d1 = (reserve1 * _shares) / totalSupply;

  1. Burn the amount of shares tokens corresponding to the amount to be withdrawn and update the balance of our reserves.

_burn(msg.sender, _shares);
_update(reserve0 – d0, reserve1 – d1);

  1. Perform the withdrawal by transferring the tokens from our AMM contract to the liquidity provider.

if (d0 > 0) {
  token0.transfer(msg.sender, d0);
}

if (d1 > 0) {
  token1.transfer(msg.sender, d1);
}

You are done! You have created your first AMM with basic functionalities.

Conclusion

AMM development is only just starting out. The AMMs that we are familiar with and use today, such as Uniswap, Curve, and PancakeSwap, are lovely in appearance but have few functions.

Future AMM designs are anticipated to be far more creative. Every DeFi user should benefit from cheaper costs, less friction, and improved liquidity as a result of this.

AMMs are fundamental elements for DeFi protocols, they can be as simple as the one we have seen in this article or much more complex as the ones used by protocols such as curve or balancer. 

For more details about the code you can visit here.