Builder Track

Smart Contract 101: Code Breakdown of Constant Product AMM

Web3 Maya
Web3 Magazine
Published in
12 min readFeb 6, 2023

--

Photo by Shubham Dhage on Unsplash

Introduction

This is the first article, in the Smart Contract 101 series. In this series, we’ll learn to read & understand smart contracts by breaking down the code into simple solidity concepts & functions. So let’s get started !!

What is Automated Market Maker (AMM) ?

An Automated Market Maker (AMM) is the underlying protocol that powers a decentralized exchange (DEX) by enabling assets to be traded by using crypto liquidity pools as counterparties, instead of a traditional market of buyers and sellers.

Digital assets that are swapped on AMMs in a permissionless manner reduce the need for intermediaries. The decentralized exchange protocols use smart contracts, liquidity pools, and liquidity providers to facilitate transactions.

There are many types of AMMs, the most popular one being Constant Product Automated Market Maker (CPAMM), which is also used by Uniswap.

In this article, we’ll be discussing CPAMM smart contract.

Constant Product AMM Smart Contract Breakdown

As solidity students, our aim should be to read smart contracts as you would read through your Twitter feed. The goal is to practice reading code as much as possible.

Today, we’ll be going through the code of a basic Constant Product Automated Market Maker (CPAMM).

I’ll first show you the whole code. I want you to try to read & understand the code on your own. This will help you track your progress through this Smart Contract 101 series.

  • If you understood everything, Great!! You don’t need to read further.
  • If you only had trouble with a few lines or functions, it will be explained soon.
  • If you didn’t understand anything at all, don’t worry. We all start learning from somewhere, so start here, start now!

We’ll go through everything in great detail & by the end of this blog you would have a better understanding of the code.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;


contract CPAMM {
IERC20 public immutable token0;
IERC20 public immutable token1;

uint public reserve0;
uint public reserve1;

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

The contract has four public variables of type “IERC20” (an interface for ERC-20 tokens), “token0” and “token1” are both IERC20 tokens. By defining “token0” and “token1” as type IERC20, the contract is specifying that these variables will be used to represent ERC-20 tokens and that the functions defined in the IERC20 interface will be available for use with these variables.

Then we have two public variables of type “uint”, “reserve0” and “reserve1” which represent the reserve balance of both tokens.

It also defines a public variable “totalSupply” of type “uint” and a mapping “balanceOf” which is a data structure that maps addresses to uint, it means that it stores the balance of each address and can be accessed by any address.

What functions would we need for this AMM? Think about Liquidity Providers & Traders from our AMMs article.

  • Firstly, we need our liquidity providers to Add Liquidity to our pool.
  • We’ll need a swap function to allow our traders to trade the two tokens.
  • Then, our Liquidity providers would want to Remove Tokens from the pool
  • And…What else? Let’s look at all the function names in our contract.
constructor(address _token0, address _token1) {
token0 = IERC20(_token0);
token1 = IERC20(_token1);
}

Our constructor takes two addresses of the ERC-20 tokens, it assigns them to token 0 & token 1 variables which are marked as public & immutable.

function _mint(address _to, uint _amount) private;
function _burn(address _from, uint _amount) private;
function _update(uint _reserve0, uint _reserve1) private;
function swap(address _tokenIn, uint _amountIn) external returns (uint amountOut);
function addLiquidity(uint _amount0, uint _amount1) external returns (uint shares);
function removeLiquidity(uint _shares) external returns (uint amount0, uint amount1);

First, let’s look at the Swap function

 function swap(address _tokenIn, uint _amountIn) external returns (uint amountOut) {
require(
_tokenIn == address(token0) || _tokenIn == address(token1),
"invalid token"
);
require(_amountIn > 0, "amount in = 0");

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

tokenIn.transferFrom(msg.sender, address(this), _amountIn);


// 0.3% fee
uint amountInWithFee = (_amountIn * 997) / 1000;
amountOut = (reserveOut * amountInWithFee) / (reserveIn + amountInWithFee);

tokenOut.transfer(msg.sender, amountOut);

_update(token0.balanceOf(address(this)), token1.balanceOf(address(this)));
}

This function allows a user to exchange one of the two supported ERC-20 tokens for the other. The user must provide the address of the token they want to exchange and the amount they want to exchange. The function first checks that the provided token address is one of the two supported tokens. Then it calculates the amount of the other token the user will receive using the current reserves of both tokens and a 0.3% fee. The function then transfers the tokens between the user and the contract, updating the contract’s internal state.

 function addLiquidity(uint _amount0, uint _amount1) external returns (uint shares) {
token0.transferFrom(msg.sender, address(this), _amount0);
token1.transferFrom(msg.sender, address(this), _amount1);


if (reserve0 > 0 || reserve1 > 0) {
require(reserve0 * _amount1 == reserve1 * _amount0, "x / y != dx / dy");
}


if (totalSupply == 0) {
shares = _sqrt(_amount0 * _amount1);
} else {
shares = _min(
(_amount0 * totalSupply) / reserve0,
(_amount1 * totalSupply) / reserve1
);
}
require(shares > 0, "shares = 0");
_mint(msg.sender, shares);

_update(token0.balanceOf(address(this)), token1.balanceOf(address(this)));

This function allows a user to add liquidity to the contract by providing equal value amounts of the two supported tokens. The function first checks that the provided amounts are equal in value to each other using the current reserves. Then it calculates the number of shares the user will receive based on the provided amounts and the current total supply. The function then transfers the tokens to the contract and mints shares for the user, updating the contract’s internal state.

 function removeLiquidity(
uint _shares
) external returns (uint amount0, uint amount1) {

// bal0 >= reserve0
// bal1 >= reserve1

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

amount0 = (_shares * bal0) / totalSupply;
amount1 = (_shares * bal1) / totalSupply;
require(amount0 > 0 && amount1 > 0, "amount0 or amount1 = 0");

_burn(msg.sender, _shares);
_update(bal0 - amount0, bal1 - amount1);

token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
}

This function allows a user to remove liquidity from the contract by providing the number of shares they own. The function calculates the equivalent amounts of the two supported tokens the user will receive based on the provided shares and the current token balances. Then it burns the shares, transferring the tokens back to the user and updating the contract’s internal state.

function _sqrt(uint y) private pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}

This function is used to calculate the square root of a number. It is used in the addLiquidity function to calculate the number of shares a user will receive when they add liquidity.

It is a private, pure function that calculates the square root of a given input, “uint y” & returns thye result as “uint z”.

The function uses the Babylonian method for approximating square root, which involves repeatedly averaging the quotient of the input & a current approximation until a desired level of accuracy is reached.

If the input is less than or equal to 3, the function returns 1 or 0, depending on whether the input is 0 or not.

 function _min(uint x, uint y) private pure returns (uint) {
return x <= y ? x : y;
}
}

This is a private, pure function in the Solidity programming language that takes in two unsigned integers (uint) as input, “x” and “y”, and returns the smaller of the two values as the output. It uses the ternary operator ( ?: ) which is a shorthand way of writing an if-else statement. The expression “x <= y” is evaluated, if it is true, then the operator returns the value of “x” and if it’s false, it returns the value of “y”.

This function is used to determine the minimum amount of a certain asset that is needed to maintain a certain constant product within the liquidity pool.

interface IERC20 {
function totalSupply() external view returns (uint);

function balanceOf(address account) external view returns (uint);

function transfer(address recipient, uint amount) external returns (bool);

function allowance(address owner, address spender) external view returns (uint);

function approve(address spender, uint amount) external returns (bool);

function transferFrom(
address sender,
address recipient,
uint amount
) external returns (bool);

event Transfer(address indexed from, address indexed to, uint amount);
event Approval(address indexed owner, address indexed spender, uint amount);
}

The interface “IERC20” is a standard interface for contracts that implement the functionality of an ERC-20 token on the Ethereum blockchain. ERC-20 is a technical standard for smart contracts that allows for the creation of tokens on the Ethereum blockchain.

In an AMM contract, this interface is used to interact with the ERC-20 token that is being traded on the market. The various functions declared in the interface, such as totalSupply(), balanceOf(), transfer(), allowance(), approve() and transferFrom() provide functionality for querying the total supply of the token, checking the balance of a particular address, transferring tokens between addresses, approving other addresses to transfer tokens on behalf of the owner, and performing token transfers on behalf of the owner.

Additionally, the two events Transfer and Approval provide a way for external parties to be notified of transfers and approvals happening on the contract.

By implementing this interface, the AMM contract can interact with the ERC-20 token in a standard way, allowing for interoperability with other contracts and wallets that also implement the ERC-20 standard. This makes it easier for users to interact with the AMM contract and for the contract to be integrated into other DeFi applications.

Interacting with our CPAMM contract

Now that we have read the code & got some idea of what these functions do, its time to interact with it & get a better understanding.

Let’s deploy this code in Remix: CPAMM Smart Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract CPAMM {
IERC20 public immutable token0;
IERC20 public immutable token1;

uint public reserve0;
uint public reserve1;

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

constructor(address _token0, address _token1) {
token0 = IERC20(_token0);
token1 = IERC20(_token1);
}

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

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

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

function swap(address _tokenIn, uint _amountIn) external returns (uint amountOut) {
require(
_tokenIn == address(token0) || _tokenIn == address(token1),
"invalid token"
);
require(_amountIn > 0, "amount in = 0");

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

tokenIn.transferFrom(msg.sender, address(this), _amountIn);

/*
How much dy for dx?

xy = k
(x + dx)(y - dy) = k
y - dy = k / (x + dx)
y - k / (x + dx) = dy
y - xy / (x + dx) = dy
(yx + ydx - xy) / (x + dx) = dy
ydx / (x + dx) = dy
*/
// 0.3% fee
uint amountInWithFee = (_amountIn * 997) / 1000;
amountOut = (reserveOut * amountInWithFee) / (reserveIn + amountInWithFee);

tokenOut.transfer(msg.sender, amountOut);

_update(token0.balanceOf(address(this)), token1.balanceOf(address(this)));
}

function addLiquidity(uint _amount0, uint _amount1) external returns (uint shares) {
token0.transferFrom(msg.sender, address(this), _amount0);
token1.transferFrom(msg.sender, address(this), _amount1);

/*
How much dx, dy to add?

xy = k
(x + dx)(y + dy) = k'

No price change, before and after adding liquidity
x / y = (x + dx) / (y + dy)

x(y + dy) = y(x + dx)
x * dy = y * dx

x / y = dx / dy
dy = y / x * dx
*/
if (reserve0 > 0 || reserve1 > 0) {
require(reserve0 * _amount1 == reserve1 * _amount0, "x / y != dx / dy");
}

/*
How much shares to mint?

f(x, y) = value of liquidity
We will define f(x, y) = sqrt(xy)

L0 = f(x, y)
L1 = f(x + dx, y + dy)
T = total shares
s = shares to mint

Total shares should increase proportional to increase in liquidity
L1 / L0 = (T + s) / T

L1 * T = L0 * (T + s)

(L1 - L0) * T / L0 = s
*/

/*
Claim
(L1 - L0) / L0 = dx / x = dy / y

Proof
--- Equation 1 ---
(L1 - L0) / L0 = (sqrt((x + dx)(y + dy)) - sqrt(xy)) / sqrt(xy)

dx / dy = x / y so replace dy = dx * y / x

--- Equation 2 ---
Equation 1 = (sqrt(xy + 2ydx + dx^2 * y / x) - sqrt(xy)) / sqrt(xy)

Multiply by sqrt(x) / sqrt(x)
Equation 2 = (sqrt(x^2y + 2xydx + dx^2 * y) - sqrt(x^2y)) / sqrt(x^2y)
= (sqrt(y)(sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(y)sqrt(x^2))

sqrt(y) on top and bottom cancels out

--- Equation 3 ---
Equation 2 = (sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(x^2)
= (sqrt((x + dx)^2) - sqrt(x^2)) / sqrt(x^2)
= ((x + dx) - x) / x
= dx / x

Since dx / dy = x / y,
dx / x = dy / y

Finally
(L1 - L0) / L0 = dx / x = dy / y
*/
if (totalSupply == 0) {
shares = _sqrt(_amount0 * _amount1);
} else {
shares = _min(
(_amount0 * totalSupply) / reserve0,
(_amount1 * totalSupply) / reserve1
);
}
require(shares > 0, "shares = 0");
_mint(msg.sender, shares);

_update(token0.balanceOf(address(this)), token1.balanceOf(address(this)));
}

function removeLiquidity(
uint _shares
) external returns (uint amount0, uint amount1) {
/*
Claim
dx, dy = amount of liquidity to remove
dx = s / T * x
dy = s / T * y

Proof
Let's find dx, dy such that
v / L = s / T

where
v = f(dx, dy) = sqrt(dxdy)
L = total liquidity = sqrt(xy)
s = shares
T = total supply

--- Equation 1 ---
v = s / T * L
sqrt(dxdy) = s / T * sqrt(xy)

Amount of liquidity to remove must not change price so
dx / dy = x / y

replace dy = dx * y / x
sqrt(dxdy) = sqrt(dx * dx * y / x) = dx * sqrt(y / x)

Divide both sides of Equation 1 with sqrt(y / x)
dx = s / T * sqrt(xy) / sqrt(y / x)
= s / T * sqrt(x^2) = s / T * x

Likewise
dy = s / T * y
*/

// bal0 >= reserve0
// bal1 >= reserve1
uint bal0 = token0.balanceOf(address(this));
uint bal1 = token1.balanceOf(address(this));

amount0 = (_shares * bal0) / totalSupply;
amount1 = (_shares * bal1) / totalSupply;
require(amount0 > 0 && amount1 > 0, "amount0 or amount1 = 0");

_burn(msg.sender, _shares);
_update(bal0 - amount0, bal1 - amount1);

token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
}

function _sqrt(uint y) private pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}

function _min(uint x, uint y) private pure returns (uint) {
return x <= y ? x : y;
}
}

interface IERC20 {
function totalSupply() external view returns (uint);

function balanceOf(address account) external view returns (uint);

function transfer(address recipient, uint amount) external returns (bool);

function allowance(address owner, address spender) external view returns (uint);

function approve(address spender, uint amount) external returns (bool);

function transferFrom(
address sender,
address recipient,
uint amount
) external returns (bool);

event Transfer(address indexed from, address indexed to, uint amount);
event Approval(address indexed owner, address indexed spender, uint amount);
}

For deploying, the CPAMM contract will take 2 ERC20 token addresses. You can use this sample ERC20 & IERC20 smart contract, to get sample code of ERC20 tokens. Make 2 ERC20 tokens by changing the token name & symbol. Compile & deploy each contract.

Copy the addresses of these two contracts, put them in CPAMM contract & you’ll be good to go.

Once you deploy the CPAMM smart contract, I want you to :

  • Add liquidity
  • Remove Liquidity
  • Swap Tokens

How to add liquidity to CPAMM?

I’ll guide you step-by-step on how to add liquidity to our CPAMM contract.

  1. Mint: Go to the mint function in your Token 0 contract & mint 500 tokens. Now, for Token 1, mint 100 tokens. Check the total supply of each token to make sure it has been minted successfully.
  2. Approve: Now go to the approve function of Token 0. You’ll see a spender field that takes an address & an amount field that takes a uint256. Copy the address of the CPAMM contract & input it as a spender. For the amount, input all the tokens you minted, i.e. 500. Do the same for Token 1.
  3. Add Liquidity: Go to the addLiquidity function of your CPAMM contract & put the approved amount of Token 0 as amount 0, i.e. 500 & the approved amount of Token 1 as amount 1, i.e. 100.
  4. Reserves & Total Supply: Once you have added liquidity, check the reserves to see if you have done everything right. reserve 0 will have 500 tokens & reserve 1 will have 100 tokens. Now check the total supply, which would be 223.

If you don’t know how an AMM works or want a refresher refer to this easy-to-understand explanation.

Now, it’s time for you to take charge. I want you to try & figure out on your own how to remove liquidity & swap tokens.

Interact & play around with these contracts to get a better understanding of them.

Conclusion

This was it for today, Thanks for reading. Hope you learned something new. I’ll be back soon with more smart contract breakdowns. Till then, take care & keep BUIDLing.

If you enjoyed the blog, please click the 👏 button and share it to help others! Feel free to leave a comment 💬 below. Have feedback? Let’s connect on Twitter.

--

--

Web3 Maya
Web3 Magazine

Blockchain Developer | Defi Enthusiast | Bug Bounty Hunter @Code4Arena | Learner | Trekker | Cat Parent