Automated Market Maker (UNISWAP)
“The term market maker refers to a firm(mainly brokerage houses that provide trading services for investors in order to keep the financial market liquid) or individual(known as a local) who actively quotes two-sided markets A two-sided market exists when both buyers and sellers meet to exchange a product or service) in a particular security by providing bids and offers (known as asks) along with the market size of each”: Investopedia. Market Makers oversees the operation of traders, when Trader A decides to buy a token for a specific price, the Market Makers ensures there’s always a ready market for that particular asset. They facilitate the process required to provide liquidity for trading pairs by offering both buy and sell prices for an asset. They earn profit through the spread(the difference between the buying and selling prices). In rare cases when a trader cannot find a buyer at that specific price, it means the asset has low liquidity(the ease of buying and selling an asset, -high liquidity means high buying and selling activity — and vice versa). When liquidity is low slippages occur (Slippage; the difference between the expected price of an asset and the price with which it was executed, occurs when there’s a change between the time order was placed and the order was fulfilled).
Automated Market making has replaced traditional market making in decentralized finance, but in an automatic and permission-less way using locked liquidity pools in a contract instead of traditional market makers (buyers and sellers). Automated market maker enables decentralized trading without relying on traditional order books and centralized intermediaries, tokens are easily swapped instantly based on the availability of liquidity in the market maker. By exchanging with the liquidity pool at a dynamically adjusted rate, determined by the two tokens in the pool, the Automated market maker ensures the product remains constant by increasing one token as the other decreases. ie, the product of the two tokens in the liquidity pool remains constant x*y=k. This formula was proposed by the founder of Ethereum Vitalik and popularized by UNISWAP, this preset mathematical equation sets the relationship between the assets held in the liquidity pools. The total amount of tokens coming into the liquidity pool must be equal to the total amount leaving the pool.dx=dy.
X*Y=K. X and Y represent two different tokens in a pool and k is the constant product of those quantities, the equation represents the product relationship in an Automated Market Maker. The constant increase of one token and the subsequent decrease of the other token allows for continuous provision of liquidity and price discovery.
For every crypto trading pair, a separate pool must be created, so traders need to locate a specific pool whenever they need a pair. Pools are funded by contributors who deposit their crypto assets to provide liquidity for others to trade against and they earn a portion of the trading fees. Eg. You deposit a percentage of the total liquidity locked token, which earns you a concurrent % of the Locked Pool token: LP = dx/x t = dy /yt.
For E.g tokenA/tokenB pair, swapping tokenA to tokenB, you deposit tokenA to the pool hence the increase in the graph, reducing the amount of tokenB in the pool hence the decrease in the graph for tokenB, due to the increase of tokenA in the pool, the price of tokenA reduces, subsequently increasing the price of tokenB. It doesn’t matter how volatile the price gets, there will eventually be a return to a state of balance that reflects a relatively accurate market price, because xy=k. But how can we determine the amount of tokenB (dy)leaving the pool, since we know the amount of tokenA sent in, the amount of tokenA and tokenB in the pool so how can we know how much tokenB relative to the amount sent in would be sent out:
The amount of token to be sent out when you swap tokenA/tokenB is calculated by dy = ydx /(x + dx), let’s work this out on how we arrived here from the graph.
Before the trade, xy=k, where x and y are two distinct tokens(tokenA and tokenB)
After the trade: (x+dx)(y-dy)=k, from the graph we can see the downward slope of the tokenB(y-dy) and the increase in tokenA(x+dx), this is to maintain the constance K
x= amount of asset A in the contract
dx= the amount of tokenA coming in
dy= amount of tokenB leaving the contract
y = amount of tokenB in the contract
k= constant
We’re looking for dy, the amount of tokens to be sent out after the swap
(x+ dx)- we adding tokenA to the contract
(y-dy) - we sending tokenB out
(x+dx)(y-dy)/x+dx=k/x+dx
y-dy=k/x+dx
recall xy=k
switching dy and replacing k with xy
so dy=y-xy / x+dx
using fractions, we multiply (y-xy) with x+dx
dy= yx + ydx - xy / x + dx
rearrange for matching alphabets
dy= yx - xy +ydx / x+dx
dy=ydx / x+dx.
When the price of an asset is trading at a discounted price in an AMM but a different price across multiple exchanges, this creates an arbitrage opportunity- the strategy of buying discounted token from a platform where it’s selling at a low price and selling it at a platform where its higher. This is most likely as a result of a user buying a huge chunk from the pool, hence reducing the price of the asset in that pool, this trade helps the pool to match the standard market rate.
Let’s take a look at the Contract implementation:
Our contract takes in two tokens, both ERC tokens, so create a solidity file with an interface to IERC20. We import the file into our Constant product Automated Market Maker file. The contract will take in two tokens, we set both tokens as immutable because they will not change. Both tokens will be initialized in the constructor.
Our contract keeps track of the two tokens in the pool, so we named them reserve0 and reserve1 respectively. totalSupply represents the total share in the pool, when a user supplied liquidity, we either mint or burn shares, and the totalSupply keeps track. balanceOf keeps track of the total shares owned by each user.
Now let’s write the functions, we’ll declare some internal functions, and I will explain the purpose of these functions much later, we start with the functions to mint and burn shares(LP) to the user, the others are min , update, and sqrt . The mint function takes two params, the address receiving the minting shares(address to) and the amount of token deposited(uint amount), the function increases the balanceOf and the totalSupply by the amount, also we have the burn function, takes two params like the mint function, the address it's burning the shares from (address from), and the amount of token deposited(uint amount), the function decreases the balanceOf and the totalSupply but the amount. The min function takes in two values and returns the minimum value, the sqrt returns the square root of the passed-in value, and the _update function, updates the reserves, this is to help keeps track of the total token0 and token1 in the pool to prevent users from manipulating their inputted figure.
function _mint(address _to, uint _amount) private {
balanceOf[_to] += _amount;
totalSupply += _amount;
}
function _burn(address _from, uint _amount) private {
balanceOf[_from] -= _amount;
totalSupply -= _amount;
}
Now let’s get to the main functions, we’ve three main functions. the swap, addLiquidity (for liquidity providers, the mint function is called when the token is deposited.), and removeLiquidity (deposit the LP shares to withdraw their tokens) functions. These are all external functions.
The swap function takes in two params, the address of the token(address tokenIn) could be either token, the amount of the token to be swapped ( uint amountIn), and returns the amount of the token to be sent out( uint amountOut). Then we check tokenIn for the input token( could be token1 or token0), we use the boolean isToken0 to check which token is sent in, if the tokenIn is token0, the istoken0 returns true, reserve0 is updated and increased, reserve1 is decreased and token1 is sent out, and then we check the _amountIn if it’s greater than 0. We pull in the token with the transferFrom keyword on solidity, To calculate the amount of token that goes out we use this function: ydx /(x + dx)= dy
Where dy= the amount of tokens going out
y= the amount of tokenOut locked in the contract
dx= the amount of token that came in
x= the amount of tokenIn that’s locked in the contract
We’re using 0.3% to calculate the fees, that's what Uniswap uses.
To calculate the amountOut using the derived function, we calculate the amountOut , then transfer it to the user and update the balances.
function swap(
address _tokenIn,
uint _amountIn
) external returns (uint amountOut) {
require(
_tokenIn == address(i_token0) || _tokenIn == address(i_token1),
"invalid Token"
);
require(_amountIn > 0, "Amount should be greater than 0");
bool isToken0 = _tokenIn == address(i_token0);
(
IERC20 tokenIn,
IERC20 tokenOut,
uint reserveIn,
uint reserveOut
) = isToken0
? (i_token0, i_token1, reserve0, reserve1)
: (i_token1, i_token0, reserve1, reserve0);
tokenIn.transferFrom(msg.sender, address(this), _amountIn);
uint amountInWithFee = (_amountIn * 997) / 1000;
amountOut =
(reserveOut * amountInWithFee) /
(reserveIn + amountInWithFee);
tokenOut.transfer(msg.sender, amountOut);
_update(
i_token0.balanceOf(address(this)),
i_token1.balanceOf(address(this))
);
}
Now we do addLiquidity and removeLiquidity functions. The addLiquidity function takes in the amount0 and amount1 of both tokens and returns the shares(LP) to the user. First were transfer both tokens in from the user, then we check that they’re more than 0, and we calculate for the liquidity shares, f = (x,y) =value of Liquidity, to calculate the shares we multiply the amount in by the totalSupply and divide it by the reserve of the appropriate token, both are passed to through the _min function, which returns the lowest value. The amount calculated is minted and sent to the user. The reserve is updated.
The removeLiquidity function takes in the amount of shares minted by the user and returns the amount of both tokens. We check for the balance of both tokens in the contract, require that the calculated amount is more than 0, we burn the amount then deduct it from the balance and update it.
Check the complete code and comments on Remix and my Github, next time we will write the unit and staging test for this contract using hardhat and VSCode.
And that's it, thanks for reading.
Credit: Solidity by example, Investopedia, Gemini