Introduction
This tutorial will guide you as a developer through the process of building an Automated Market Maker (AMMs) on the Celo blockchain. Before we proceed, let us take a look at what Market Makers are. Market Makers are also called Liquidity Providers. They are companies that facilitate liquidity provision and trading in financial markets. Basically, they act as middle men that maintain an inventory of tradable assets. The employ the use of human traders and algorithms to carry out the trading activities and ensure that there is always liquidity in the market. One very popular market making company is Goldman Sachs. With this background knowledge, we can define Automated Market Makers as decentralized platforms or exchanges that facilitate liquidity provision and trading on the blockchain. AMMs do not need order books to operate instead they use liquidity pools. Uniswap is one of the leading DeXs that implements the concept of AMMs.
Background Knowledge
Here is a list of important things about AMMs you should know before building one
- Liquidity Pool: A liquidity pool is a collection of digital assets or funds that is held in a smart contract. These funds are provided by individuals or entities refered to as liquidity providers.
- Liquidity Provider: A liquidity provider refers to individuals or entities that deposit tokens into the liquidity pool thus serving as fund contributors.
- Constant Product Formula: This formula is a mathematical equation that ensures that the total assets value in a liquidity pool remains constant. The formula is defined below
(x * y = k)
- Token Swaps: This is defined as the process of exchanging one token for another within a liquidity pool. The price during token swapping is usually dependent on the constant product formula and the current token balance available in the liquidity pool.
- Trading Fees: Most AMMs charge fees on every trade done. The fees varies across AMMs
Process Flow
Starter Guide
The codebase for this tutorial is available here. You could also check out Uniswap
Setting Up The Development Environment
For this tutorial, you need to have the following installed on your computer
- A package manager such as
npm
oryarn
- Node js
- Celo Composer
Celo Composer
Celo Composer is built on Celo to make it simple to build dApps using a variety of front-end frameworks, and libraries. For more information on how to start off your project with Celo Composer, visit here. Please keep in mind that these are the frameworks you should select while installing Celo Composer
- React
- Rainbow-kit celo
- Hardhat
- Subgraph
If you are stuck, kindly refer to the Celo Composer installation section of my previous tutorial here.
Smart Contract
From your celo-composer template, navigate to the packages/hardhat/contracts folder and create a solidity file. Our smart contract will address major concepts such as creating a liquidity pool, swapping tokens, calculating the constant product and calculating trading fees. Use this code to guide you on how your contract should look like
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract AmmSwap {
// The constant product.
uint256 constantProduct;
// The mapping of token addresses to balances.
mapping(address => uint256) balances;
// The mapping of token addresses to trading fees.
mapping(address => uint256) fees;
// The constructor.
constructor() {
constantProduct = 1e18;
}
// The function to create a liquidity pool.
function createLiquidityPool(
address tokenA,
address tokenB,
uint256 amountA,
uint256 amountB
) public returns (uint256 liquidity) {
// Check that the amounts are not zero.
require(amountA > 0 && amountB > 0, "Amounts must be greater than zero");
// Calculate the liquidity.
liquidity = amountA * amountB;
// Add the liquidity to the pool.
balances[tokenA] += amountA;
balances[tokenB] += amountB;
// Return the liquidity.
return liquidity;
}
// The function to swap tokens.
function swap(
address tokenA,
address tokenB,
uint256 amountA,
uint256 minAmountB
) public returns (uint256 amountB) {
// Check that the amounts are not zero.
require(amountA > 0 && minAmountB > 0, "Amounts must be greater than zero");
// Calculate the amount of B that will be received.
amountB = calculateAmountB(amountA, minAmountB);
// Check that the amount of B is not zero.
require(amountB > 0, "Insufficient liquidity");
// Swap the tokens.
swapTokens(tokenA, amountA, tokenB, amountB);
// Return the amount of B that was received.
return amountB;
}
function swapTokens(
address tokenA,
uint256 amountA,
address tokenB,
uint256 amountB
) internal {
// Check that the amounts are not zero.
require(amountA > 0 && amountB > 0, "Amounts must be greater than zero");
// Calculate the new constant product.
uint256 newConstantProduct = constantProduct - amountA * amountB;
// Update the balances of the pool.
balances[tokenA] -= amountA;
balances[tokenB] += amountB;
// Update the constant product.
constantProduct = newConstantProduct;
// Transfer the tokens to the user.
(bool success, ) = tokenA.call(abi.encodePacked(amountA));
require(success, "Failed to transfer tokens");
(success, ) = tokenB.call(abi.encodePacked(amountB));
require(success, "Failed to transfer tokens");
}
// The function to calculate the amount of B that will be received in a swap.
function calculateAmountB(uint256 amountA, uint256 minAmountB) public view returns (uint256) {
// Calculate the constant product.
uint256 newConstantProduct = constantProduct - amountA * minAmountB;
// Calculate the amount of B that will be received.
uint256 amountB = newConstantProduct / amountA;
// Ensure that the amount of B is not less than the minimum amount.
amountB = amountB < minAmountB ? minAmountB : amountB;
// Return the amount of B.
return amountB;
}
// The function to calculate the trading fee.
function calculateTradingFee(uint256 amount) public pure returns (uint256) {
// The trading fee is 0.03%.
uint256 fee = amount * 3e18 / 10000;
// Return the trading fee.
return fee;
}
// The function to calculate the price of a token.
function calculatePrice(address tokenA, address tokenB) public view returns (uint256) {
// Get the amounts of tokens in the pool.
uint256 amountA = balances[tokenA];
uint256 amountB = balances[tokenB];
// Calculate the constant product.
uint256 constantProduct = amountA * amountB;
// Calculate the price of token A.
uint256 priceA = constantProduct / amountB;
// Return the price of token A.
return priceA;
}
}
From this contract, users can create liquidity pools by depositing tokens into the contract. This is what will be used to maintain the liquidity pool. The contract also automatically calculates the price of a swap and executes it with the swap function. You can track the price of tokens by calling the calculatePrice
function on the contract, that way it’s able to return the current price of the tokens.
To test this contract, let us go to the Remix Online IDE. Remix Online IDE is a tool for developing, debugging, deploying and testing smart contracts. Open Remix and create a file under the contracts folder. We are literally copying the same smart contract code as above
From here navigate to the Solidity compiler tab to compile the code
On a successful compilation, Remix will give several options such as publishing on ipfs or Swarm.
We don’t need to do those so we can move to the next tab and deploy our contract.
Click on the Deploy
button
The first test we will do is create the liquidity pool
We will input the token addresses of tokenA and tokenB as well as the amount of the tokens. You can get test addresses from Remix from Account, so just navigate, copy and paste any two addresses
Click on transact
and observe your output
From the image above, decoded output tells us how much liquidity we have of which it is 1000
The second test we can do is the swap function
Fill in all inputs and observe the output.
The third test we can do is calculate the price of a token after getting the amounts of tokens in the pool and calculating the constant product.
Now, that we have ran couple of tests, let’s go back to our IDE and deploy our smart contract.
To deploy your contract, write a deploy script in the deploy folder
const hre = require("hardhat");
const main = async () => {
const contractFactory = await hre.ethers.getContractFactory('AmmSwap');
const contract = await contractFactory.deploy();
await contract.deployed();
console.log("Contract deployed to this address:", contract.address);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
You can now head back to your terminal to run the following command
npx hardhat --network alfajores run deploy/deploy.js
Your output should look similar to this
Contract deployed to this address: 0x3E73365764fBBE4f58Eb0B6a62CA80FC706D851B
The next process is to verify your contract address with this command
npx hardhat --network alfajores verify 0x3E73365764fBBE4f58Eb0B6a62CA80FC706D851B
Finally, your output should look like this after the verification
Nothing to compile
Successfully submitted source code for contract
contracts/AmmSwap.sol:AmmSwap at 0x3E73365764fBBE4f58Eb0B6a62CA80FC706D851B
for verification on the block explorer. Waiting for verification result...
Successfully verified contract AmmSwap on Etherscan.
https://alfajores.celoscan.io/address/0x3E73365764fBBE4f58Eb0B6a62CA80FC706D851B#code
Conclusion
I hope you enjoyed reading this article. Here is a recap of what you learnt in this article
Overview of AMMs, basic terms and process flow
Understanding the constant product formula (x * y = k)
Using Remix IDE and its features
Implementing the AMM functionality using Solidity
Testing the functionality of the AMM contract using Remix
Deploying of the AMM contract via Celo composer
If you are interested in seeing this getting implemented from a UI point of view, feel free to read my next article here