Vyper, Python, web3.eth, eth-tester



Building an Automated Market Maker (AMM) smart contract for trading ERC-20 tokens using a constant product invariant formula (similar to Uniswap ).


What is a Smart Contract?

A smart contract is a piece of computer software that is designed as an automated self-enforcing contract, which means it triggers certain action after predetermined conditions are met. Smart contracts can be used, for instance, as digital agreements that intermediate the exchange of cryptocurrencies (or any other digital asset) between two parties. Once the terms of the agreement have been set, the smart contract verifies their fulfillment, and the assets are distributed in accordance.

Smart contracts play an important role in the blockchain space and cryptocurrency markets, particularly in regard to  ERC-20  tokens, which represent a class of tokens created on the Ethereum network that follow the ERC-20 standard. The use of smart contracts enables a trustless and cost-effective exchange of these tokens for decentralized exchanges (DEXs). Their use can also facilitate payment processing for decentralized applications (DApps) and Initial Coin Offering (ICO) events.

Despite their obvious applications in finance, smart contracts are versatile enough to apply to practically any industry in which funds, digital assets, or any kind of digital information need to be transferred between parties.




Smart Contract Overview



What is an AMM?

An automated market maker (AMM) is a type of decentralized exchange (DEX) protocol that relies on a mathematical formula to price assets. An AMM works similarly to an order book exchange in that there are trading pairs – for example, USDC/DAI. However, you don’t need to have a counterparty (another trader) on the other side to make a trade. Instead, you interact with a smart contract that “makes” the market for you, with assets are priced according to an algorithm.


Liquidity Pools

Liquidity refers to how easily one asset can be converted into another asset without affecting its market price. Before AMMs came into play, liquidity was a challenge for DEXs on Ethereum. As a new technology, the number of buyers and sellers was small, which meant it was difficult to find enough people to trade on a regular basis. AMMs fix this problem of limited liquidity by creating liquidity pools and offering liquidity providers the incentive to supply these pools with assets. The more assets in a pool and the more liquidity the pool has, the easier trading becomes on DEXs.

On AMM platforms, instead of trading between buyers and sellers, users trade against a pool of tokens — a liquidity pool. At its core, a liquidity pool is a shared pot of tokens. Users supply liquidity pools with tokens and the price of the tokens in the pool is determined by a mathematical formula.


Constant Product Formula

You can think of an AMM as a primitive robotic market maker that is always willing to quote prices between two assets according to a simple pricing algorithm. Most AMMs use a ‘Constant Product Market Maker’- a mathematical  function  that algorithmically determines the price of an asset based on the ratio of assets within the liquidity pool. It prices the two assets so that the number of units it holds of each asset, multiplied together, is always equal to a fixed constant.

That is, if the contract owns some units of token x and some units of token y, it prices any trade so that the final quantities of x and y it owns, multiplied together, are equal to a fixed constant, k. This is formalized as the constant product equation: x * y = k.


Let’s say we fund a constant product AMM pool with 50 apples (a) and 50 bananas (b), so anyone is free to pay apples for bananas or bananas for apples. Let’s assume the exchange rate between apples and bananas is exactly 1:1 on their primary market. Because the pool holds 50 of each fruit, the constant product rule gives us a * b = 2500. Therefore, for any trade, the AMM must maintain the invariant that our inventory of fruit, multiplied together, equals 2500.

So, let’s say a customer comes to our pool to buy an apple. How many bananas will they need to pay?

If they buy an apple, our pool will be left with 49 apples, but 49*b has to equal 2500. Solving for b, we get 51.02 total bananas. Since we already have 50 bananas in inventory, we’ll need 1.02 extra bananas for that apple, so the price we have to quote them is 1.02 bananas/apple for 1 apple.

Note that this is close to the natural price of 1:1! Because it’s a small order, there is only a little slippage. But what if the order is larger?


Larger Order

If they want to buy 10 apples, we would charge them 12.5 bananas for a unit price of 1.25 bananas/apple for 10 apples.

And if they wanted a huge order of 25 apples (half of all the apples in inventory), the unit price would be 2 bananas/apple! (You can intuit this because if one side of the pool halves, the other side needs to double.)

The important thing to realize is that the AMM cannot deviate from this pricing curve. If someone wants to buy some apples and later someone else wants to buy some bananas, the AMM will sweep back and forth through this pricing curve, wherever demand carries it.


Recovery via Arbitrage

Now here’s the kicker: if the true exchange rate between apples and bananas is 1:1, then after the first customer purchases 10 apples, our pool will be left with 40 apples and 62.5 bananas. If an arbitrageur then steps in and buys 12.5 bananas, returning the pool back to its original state, the AMM charges them a unit price of only 0.8 apples/banana.

The AMM underprices the bananas! Since we are now heavy on bananas, the price of bananas is discounted by the algorithm to attract apples and rebalance the inventory.

AMMs are constantly performing this oscillation — slightly moving off the real exchange rate, then sashaying back in line thanks to arbitrageurs.



Supply maintains constant product k



External Functions


Conceptually, the lifecycle of an AMM has three main parts:

  1. A “Liquidity Provider” deposits two types of tokens into the contract.
  2. provideLiquidity(tokenA_addr, tokenA_amount, tokenB_addr, tokenB_amount)

    tokenA_addr and tokenB_addr are addresses of valid ERC20 contracts. tokenA_amount and tokenB_amount represent the quantities of each token that are being deposited. The sender corresponds to the address which provides liquidity (and therefore is the owner).


  3. Users trade one type of token for the other.
  4. tradeTokens(token_addr, amount)

    token_addr must match either tokenA_addr or tokenB_addr. amount is the amount of that token being traded to the contract. The contract calculates the amount of the other token to return to the sender using the constant product invariant formula.


  5. The Liquidity Provider closes the contract and withdraws all tokens.
  6. withdraw()

    If the message sender was the initial liquidity provider, this gives all tokens held by the contract to the message sender, otherwise it fails.


Testing

The contract has been tested using the  eth-tester  testing environment. Eth-tester is  integrated  with Web3 and allows for smart contract testing without having to deal with the network delay and overhead of interacting with a live network.




Testing Output



Why use Vyper?

The Ethereum Virtual Machine (EVM) executes byte code, and both Solidity and Vyper are high-level languages that can compile to EVM-compatible byte code. Vyper  is logically similar to Solidity, and syntactically similar to Python. Vyper was chosen because it is more transparent for all parties and it deliberately has a lesser feature set than Solidity, ensuring contract security (it lacks modifiers, class inheritance, recursive calls, and a few other features that proved to be problematic in Solidity). The actual   Uniswap contract   is written in Vyper.


See the code

Access to private GitHub  repository  can be provided upon request.



Let's work together!

Like my work? I'm happy to connect.