DEV Community

Cover image for Create an Automated Market Maker (AMM) with Vyper! ๐Ÿ
Rafael Abuawad
Rafael Abuawad

Posted on • Updated on

Create an Automated Market Maker (AMM) with Vyper! ๐Ÿ

Introduction

Welcome, in this tutorial, we will learn how to build an AMM with features like adding liquidity, removing liquidity & swapping tokens with shares and fees. The first smart contract can take in any two ERC20 tokens and make a liquidity pool with those, and the second one is used to deploy liquidity pools. We are going to use vyper to write the entirety of the smart contacts.

Prerequisites

  • Basic programming and console/terminal use experience
  • A simple understanding of Solidity or Vyper

Requirements

What is an AMM?

Automated market makers (AMMs) are part of the decentralized finance (Defi) ecosystem. They allow digital assets to be traded in a permissionless and automatic way by using liquidity pools rather than a traditional market of buyers and sellers. AMM users supply liquidity pools with crypto tokens, whose prices are determined by a constant mathematical formula. Liquidity pools can be optimized for different purposes, and are proving to be an important instrument in the Defi ecosystem. In this tutorial, we are going to use a constant-product AMM.

Setup

We are going to use Brownie in this tutorial, after you installed Brownie, create a new folder called avalanche-swap and inside it run the following command:

$ brownie init
Enter fullscreen mode Exit fullscreen mode

Also, we are going to be using the Hardhat node to test our smart contracts, so inside your project folder, run the following command:

$ npm install --save-dev hardhat
Enter fullscreen mode Exit fullscreen mode

Implementing the pair smart-contract

Let's start with the boilerplate code. We create a file named contracts/AvaSwapPair.vy, define the vyper version as >= 0.3.7, and import the ERC20 and ERC20 Detailed interfaces from vyper.

# @version ^0.3.7

from vyper.interfaces import ERC20
from vyper.interfaces import ERC20Detailed

implements: ERC20
implements: ERC20Detailed
Enter fullscreen mode Exit fullscreen mode

This is our basic boilerplate, we need the ERC20 definition to allow our users to use their shares however they want.

# @version ^0.3.7

from vyper.interfaces import ERC20
from vyper.interfaces import ERC20Detailed

###### EVENTS ######

event Transfer:
    sender: indexed(address)
    receiver: indexed(address)
    value: uint256

event Approval:
    owner: indexed(address)
    spender: indexed(address)
    value: uint256

###### STATE ######

# Share's token name
name: public(String[32])

# Share's token symbol
symbol: public(String[32])

# Share's token decimals
decimals: public(uint8)

# By declaring `balanceOf` as public, vyper automatically generates a 'balanceOf()' getter
# method to allow access to account balances.
balanceOf: public(HashMap[address, uint256])

# By declaring `allowance` as public, vyper automatically generates the `allowance()` getter
allowance: public(HashMap[address, HashMap[address, uint256]])

# By declaring `totalSupply` as public, we automatically create the `totalSupply()` getter
totalSupply: public(uint256)

###### CONSTRUCTOR ######
@external
def __init__():
    self.name = "Avalanche Swap"
    self.symbol = "AVAS"
    self.decimals = 18


###### INTERNAL METHODS ######
@internal
def _mint(_to: address, _value: uint256):
    assert _to != empty(address), "ERC20: mint to the zero address"
    self.balanceOf[_to] += _value
    self.totalSupply += _value
    log Transfer(empty(address), _to, _value)


@internal
def _burn(_to: address, _value: uint256):
    assert _to != empty(address), "ERC20: burn to the zero address"
    self.balanceOf[_to] -= _value
    self.totalSupply -= _value
    log Transfer(_to, empty(address), _value)


###### EXTERNAL METHODS ######
@external
def transfer(_to : address, _value : uint256) -> bool:
    assert _to != empty(address), "ERC20: transfer to the zero address"

    self.balanceOf[msg.sender] -= _value
    self.balanceOf[_to] += _value
    log Transfer(msg.sender, _to, _value)
    return True


@external
def transferFrom(_from : address, _to : address, _value : uint256) -> bool:
    self.allowance[_from][msg.sender] -= _value
    self.balanceOf[_from] -= _value
    self.balanceOf[_to] += _value

    log Transfer(_from, _to, _value)
    return True


@external
def approve(_spender : address, _value : uint256) -> bool:
    self.allowance[msg.sender][_spender] = _value
    log Approval(msg.sender, _spender, _value)
    return True


@external
def burn(_value: uint256):
    self._burn(msg.sender, _value)

Enter fullscreen mode Exit fullscreen mode

This is a basic ERC20 smart contract, we are going to need this basic functionality to implement a proper AMM. Our users will receive shares each time they deposit liquidity, each share is an ERC20 token. And can redeem these shares for the tokens that they deposited with an extra tip for doing so.

The smart contract is divided on:

  • EVENTS
  • STATE
  • INTERNAL METHODS
  • EXTERNAL METHODS

This part is important since we are going to jump between those. So take note so you don't get lost.

We would be set on the ERC20 functionality of our smart contract to manage the pool shares we are going to distribute to our liquidity providers, this part is really common so we are not going to go into detail. Check out this blog post that I made on how to create a wrapped token using a similar approach, here I explain how ERC20 tokens work, why they are important, and how we can create a custom one linked to a cryptocurrency.

After that is done we need to add a few state variables to our smart contract to add the AMM functionality. Let's start with the tokens and the reserves:

###### STATE ######
...

# Pair tokens
token0: public(address)
token1: public(address)

# Reserve records
reserve0: public(uint256)
reserve1: public(uint256)

...
Enter fullscreen mode Exit fullscreen mode

The token variables will keep track of which tokens the pool handles and the reserves will keep track of the amount that we have for each token.

Now we also need an internal update method to update the reserves each time we add or remove liquidity.

###### INTERNAL METHODS ######
...

@internal
def _update(_reserve0: uint256, _reserve1: uint256):
    self.reserve0 = _reserve0
    self.reserve1 = _reserve1

...
Enter fullscreen mode Exit fullscreen mode

Finally, we need to add a setup function and set up a state variable flag that is going to be called by that factory, this function will set the tokens and with the flag, it can only be called once.

###### STATE ######
...

# Setup flag
_setup: bool

...
Enter fullscreen mode Exit fullscreen mode

Since the method will be called from the outside we need to define it in the external methods section.

###### EXTERNAL METHODS ######
...

@external
def setup(_token0: address, _token1: address):
    assert self._setup == False, "Avalanche Swap Pair: already initialized"
    self.token0 = _token0
    self.token1 = _token1
    self._setup = True

...
Enter fullscreen mode Exit fullscreen mode

Add liquidity

Adding liquidity allows our AMM to work, without liquidity, there would not be any swapping of tokens. The addLiquidity method is really simple, the user deposits some tokens to our smart contract (the initial k=x*y is defined by the first deposit) in return the smart contract mints some share tokens to redeem these tokens later, with some collected fees as a result of other users swapping tokens with this liquidity.

We added a reentrancy guard to prevent bad actors from exploiting this functionality.

###### EXTERNAL METHODS ######
...

@external
@nonreentrant("add")
def addLiquidity(_amount0: uint256, _amount1: uint256) -> uint256:
    # add liquidity
    ERC20(self.token0).transferFrom(msg.sender, self, _amount0)
    ERC20(self.token1).transferFrom(msg.sender, self, _amount1)

    # keep the pool balanced (k=x*y)
    if self.reserve0 > 0 or self.reserve1 > 0:
        assert self.reserve0 * _amount1 == self.reserve1 * _amount0, "Avalanche Swap Pair: x / y != dx / dy" 

    shares: uint256 = 0
    if self.totalSupply == 0:
        shares = isqrt(_amount0 * _amount1)
    else:
        shares = min(
            (_amount0 * self.totalSupply) * self.reserve0,
            (_amount1 * self.totalSupply) * self.reserve1
        )

    assert shares > 0, "Avalanche Swap Pair: shares were zero"

    # mint shares to liquidity provider
    self._mint(msg.sender, shares)

    # update reserves
    self._update(ERC20(self.token0).balanceOf(self), ERC20(self.token1).balanceOf(self))

    return shares

...
Enter fullscreen mode Exit fullscreen mode

Remove liquidity

Removing the liquidity is straightforward, we burn the shares that the user passes in and we transfer the corresponding tokens to that user.

We added a reentrancy guard to prevent bad actors from exploiting this functionality.

###### EXTERNAL METHODS ######
...

@external
@nonreentrant("remove")
def removeLiquidity(_shares: uint256) -> (uint256, uint256):
    _token0: ERC20 = ERC20(self.token0)
    _token1: ERC20 = ERC20(self.token1)

    bal0: uint256 = _token0.balanceOf(self)
    bal1: uint256 = _token1.balanceOf(self)

    amount0: uint256 = (_shares * bal0) / self.totalSupply
    amount1: uint256 = (_shares * bal1) / self.totalSupply

    # _burn checks if the user has enough shares
    self._burn(msg.sender, _shares)
    # update reserves
    self._update(bal0 - amount0, bal1 - amount1)

    # transfers the tokens back
    _token0.transfer(msg.sender, amount0)
    _token1.transfer(msg.sender, amount1)

    return (amount0, amount1)

...
Enter fullscreen mode Exit fullscreen mode

Swap

The swap functionality is essential on any AMM, here a user passes a token and an amount, and with these two we return the opposite token of the pair with the corresponding amount, taking a small fee for our liquidity providers in the process.

We added a reentrancy guard to prevent bad actors from exploiting this functionality.

###### EXTERNAL METHODS ######
...

@external
@nonreentrant("swap")
def swap(_tokenIn: address, _amountIn: uint256) -> uint256:
    assert _tokenIn == self.token0 or _tokenIn == self.token1, "Avalanche Swap Pair: invalid token"
    assert _amountIn > 0, "Avalanche Swap Pair: amount in is zero"

    # variables to interact with the liquidity pool
    tokenIn: ERC20 = empty(ERC20)
    tokenOut: ERC20 = empty(ERC20)
    reserveIn: uint256 = 0
    reserveOut: uint256 = 0

    # determine which token is being swapped in
    # and assigning variables accordingly
    isToken0: bool = _tokenIn == self.token0
    if isToken0:
        tokenIn = ERC20(self.token0)
        tokenOut = ERC20(self.token1)
        reserveIn = self.reserve0
        reserveOut = self.reserve1
    else:
        tokenIn = ERC20(self.token1)
        tokenOut = ERC20(self.token0)
        reserveIn = self.reserve1
        reserveOut = self.reserve0

    # transfer in the tokens
    tokenIn.transferFrom(msg.sender, self, _amountIn)

    # 0.3% fee
    amountInWithFee: uint256 = (_amountIn * 997) / 1000

    # calculate tokens to transfer
    amountOut: uint256 = (reserveOut * amountInWithFee) / (reserveIn + amountInWithFee)
    tokenOut.transfer(msg.sender, amountOut)

    # update reserves
    self._update(ERC20(self.token0).balanceOf(self), ERC20(self.token1).balanceOf(self))

    # transfer in the tokens
    return amountOut

...
Enter fullscreen mode Exit fullscreen mode

Implementing the factory smart-contract

Let's start with the boilerplate code. We create a file named contracts/AvaSwapFactory.vy, and define the vyper version as 0.3.7. This smart contract is responsible for creating liquidity pools, using two tokens that the user can pass in.

Your smart contract should be looking something like this:

# @version ^0.3.7

###### INTERFACES ######

###### STATE ######

###### CONSTRUCTOR ######

###### METHODS ######

Enter fullscreen mode Exit fullscreen mode

This is empty for now.

First, we are going to define an interface of the pair smart contract that we built previously, the interface itself only needs one method declared, the setup method, since that is what we are going to use to define a pair.

...
###### INTERFACES ######

interface AvaSwapPair:
    def setup(_token0: address, _token1: address): nonpayable

...
Enter fullscreen mode Exit fullscreen mode

After that we are going to declare the state of the smart contract, we need to map the token pairs that we have created, and we need to add an array to store each one of them, also a master pair contract address, this is going to be used as a blueprint by vyper to create and deploy new pair smart contracts.

###### STATE ######
...

# Pair Contract => Token <=> Token
getPair: public(HashMap[address, HashMap[address, address]])

# All pairs list
allPairs: public(DynArray[address, 30])

# AvaSwap Pair contract address used to create clones of it
pairContract: address

...
Enter fullscreen mode Exit fullscreen mode

After that we are going to define the constructor, this part is really important since here is where we pass the address to an empty pair smart contract that we need to deploy before we deploy this one.

###### CONSTRUCTOR ######
...

@external
def __init__(_pairContract: address):
    self.pairContract = _pairContract

...
Enter fullscreen mode Exit fullscreen mode

And that would be everything we need before defining the main createPair method, which will allow our users to create a token pair freely.

Create pair

The create pair method is simple, we pass in two tokens, token A and token B, with these two tokens we deploy a new pair smart contract (using the pairContract state variable we defined in the constructor) and we added to the pairs mapping and the pairs array, and that's it!

###### METHODS ######
...

@external
def createPair(_tokenA: address, _tokenB: address) -> address:
    assert _tokenA != _tokenB, "Avalanche Swap Factory: identical addresses"
    assert _tokenA != empty(address) and _tokenB != empty(address), "Avalanche Swap Factory: zero address"
    assert self.getPair[_tokenA][_tokenB] == empty(address),  "Avalanche Swap Factory: pair exists"

    # create pair smart contract
    pair: address = create_forwarder_to(self.pairContract)
    AvaSwapPair(pair).setup(_tokenA, _tokenB)

    # populate mapping in, and in the reverse direction
    self.getPair[_tokenA][_tokenB] = pair
    self.getPair[_tokenB][_tokenA] = pair

    # append pair to all pairs array
    self.allPairs.append(pair)

    return pair

...
Enter fullscreen mode Exit fullscreen mode

Testing using Brownie

In this tutorial, we are going to use Brownie to test our smart contracts, you can use any other smart contract framework if you are familiar with it Hardhat or ApeWorx both have support for developing and deploying vyper smart contracts.

For this, we are going to create an extra mock ERC20 smart contract, that we are going to use to deploy mocks tokens to test our application.

Create a mock directory inside our contracts folder, and then create an ERC20.vy smart contract inside it.

The contents of that smart contract should look something like this:

# @version ^0.3.7
# @dev Implementation of ERC-20 token standard.

from vyper.interfaces import ERC20
from vyper.interfaces import ERC20Detailed

implements: ERC20
implements: ERC20Detailed

event Transfer:
    sender: indexed(address)
    receiver: indexed(address)
    value: uint256

event Approval:
    owner: indexed(address)
    spender: indexed(address)
    value: uint256

name: public(String[32])
symbol: public(String[32])
decimals: public(uint8)
totalSupply: public(uint256)
minter: address


@external
def __init__(_name: String[32], _symbol: String[32], _decimals: uint8, _supply: uint256):
    init_supply: uint256 = _supply * 10 ** convert(_decimals, uint256)
    self.name = _name
    self.symbol = _symbol
    self.decimals = _decimals
    self.balanceOf[msg.sender] = init_supply
    self.totalSupply = init_supply
    self.minter = msg.sender
    log Transfer(empty(address), msg.sender, init_supply)


@external
def transfer(_to : address, _value : uint256) -> bool:
    self.balanceOf[msg.sender] -= _value
    self.balanceOf[_to] += _value
    log Transfer(msg.sender, _to, _value)
    return True


@external
def transferFrom(_from : address, _to : address, _value : uint256) -> bool:
    self.balanceOf[_from] -= _value
    self.balanceOf[_to] += _value

    self.allowance[_from][msg.sender] -= _value
    log Transfer(_from, _to, _value)
    return True


@external
def approve(_spender : address, _value : uint256) -> bool:
    self.allowance[msg.sender][_spender] = _value
    log Approval(msg.sender, _spender, _value)
    return True


@external
def mint(_to: address, _value: uint256):
    assert msg.sender == self.minter
    assert _to != empty(address)
    self.totalSupply += _value
    self.balanceOf[_to] += _value
    log Transfer(empty(address), _to, _value)


@internal
def _burn(_to: address, _value: uint256):
    assert _to != empty(address)
    self.totalSupply -= _value
    self.balanceOf[_to] -= _value
    log Transfer(_to, empty(address), _value)


@external
def burn(_value: uint256):
    self._burn(msg.sender, _value)


@external
def burnFrom(_to: address, _value: uint256):
    self.allowance[_to][msg.sender] -= _value
    self._burn(_to, _value)
Enter fullscreen mode Exit fullscreen mode

After that go to our tests folder and create a conftest.py here we will define all of our fixtures (fixtures act like functions that return a new deployment each time) to test our application. We only need 2 demo tokens to test our AMM.

#!/usr/bin/python3

import pytest


# Demo token for testing purposes only
@pytest.fixture(scope="function")
def test_token(ERC20, accounts):
    return ERC20.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]})


# Demo token for testing purposes only
@pytest.fixture(scope="function")
def demo_token(ERC20, accounts):
    return ERC20.deploy("Demo Token", "DEMO", 18, 1e21, {'from': accounts[0]})


@pytest.fixture(scope="function")
def ava_swap_pair(AvaSwapPair, accounts):
    return AvaSwapPair.deploy({'from': accounts[0]})


@pytest.fixture(scope="function")
def ava_swap_factory(AvaSwapFactory, accounts, ava_swap_pair):
    return AvaSwapFactory.deploy(ava_swap_factory.address, {'from': accounts[0]})
Enter fullscreen mode Exit fullscreen mode

After that is setup we can test the main functionality of our AMM, mainly:

  • Create a liquidity pool (pair)
  • Add liquidity
  • Remove liquidity
  • Swap and collect fees using shares

Now create a file inside our tests folder, called test_amm.py, this will test all the functionality that we defined earlier.

import brownie 
from brownie import AvaSwapPair, Wei
Enter fullscreen mode Exit fullscreen mode

Your file should be looking like that before we begin writing tests. Here we import the AvaSwapPair contract container to interact with the pairs that we create and Wei to manipulate token amounts easily.

Create a liquidity pool

def test_create_pair(accounts, test_token, demo_token, ava_swap_factory):
    # create pair
    tx_create_pair = ava_swap_factory.createPair(test_token.address, demo_token.address, {"from": accounts[0]}) 
    # we access the created pair address
    pair_address = tx_create_pair.return_value
    # we access the pair smart-contract
    pair = AvaSwapPair.at(pair_address)

    # test default pair values
    assert pair.token0() == test_token.address
    assert pair.token1() == demo_token.address
    assert pair.totalSupply() == 0
    assert pair.reserve0() == 0
    assert pair.reserve1() == 0
Enter fullscreen mode Exit fullscreen mode

Add liquidity

def test_add_liquidity(accounts, test_token, demo_token, ava_swap_factory):
    # create pair
    tx_create_pair = ava_swap_factory.createPair(test_token.address, demo_token.address, {"from": accounts[0]}) 
    pair_address = tx_create_pair.return_value
    pair = AvaSwapPair.at(pair_address)

    amount = Wei("1000 ether")

    # approve pair to use our tokens
    test_token.approve(pair_address, amount, {"from": accounts[0]})
    demo_token.approve(pair_address, amount, {"from": accounts[0]})

    # add liquidity
    pair.addLiquidity(amount, amount, {"from": accounts[0]})

    assert pair.balanceOf(accounts[0]) == amount # minted shares
    assert pair.reserve0() == amount
    assert pair.reserve1() == amount
    assert test_token.balanceOf(pair) == amount
    assert demo_token.balanceOf(pair) == amount
Enter fullscreen mode Exit fullscreen mode

Remove liquidity

def test_remove_liquidity(accounts, test_token, demo_token, ava_swap_factory):
    # create pair
    tx_create_pair = ava_swap_factory.createPair(test_token.address, demo_token.address, {"from": accounts[0]}) 
    pair_address = tx_create_pair.return_value
    pair = AvaSwapPair.at(pair_address)

    amount = Wei("1000 ether")

    # approve pair to use our tokens
    test_token.approve(pair_address, amount, {"from": accounts[0]})
    demo_token.approve(pair_address, amount, {"from": accounts[0]})

    # add liquidity
    pair.addLiquidity(amount, amount, {"from": accounts[0]})

    assert pair.balanceOf(accounts[0]) == amount # minted shares
    assert pair.reserve0() == amount
    assert pair.reserve1() == amount
    assert test_token.balanceOf(pair) == amount
    assert demo_token.balanceOf(pair) == amount

    # get shares
    shares = pair.balanceOf(accounts[0])

    # remove liquidity
    pair.removeLiquidity(shares, {"from": accounts[0]})

    assert pair.balanceOf(accounts[0]) == 0 # minted shares
    assert pair.reserve0() == 0
    assert pair.reserve1() == 0
    assert test_token.balanceOf(pair) == 0
    assert demo_token.balanceOf(pair) == 0
Enter fullscreen mode Exit fullscreen mode

Swap and collect fees using shares

def test_remove_liquidity(accounts, test_token, demo_token, ava_swap_factory):
    # create pair
    tx_create_pair = ava_swap_factory.createPair(test_token.address, demo_token.address, {"from": accounts[0]}) 
    pair_address = tx_create_pair.return_value
    pair = AvaSwapPair.at(pair_address)

    amount = Wei("1000 ether")
    swap_amount = Wei("100 ether")

    # approve pair to use our tokens
    test_token.approve(pair_address, amount, {"from": accounts[0]})
    demo_token.approve(pair_address, amount, {"from": accounts[0]})

    # add liquidity
    pair.addLiquidity(amount, amount, {"from": accounts[0]})

    # initial balance before swap
    initial_balances = {
        "test_token": test_token.balanceOf(accounts[0]),
        "demo_token": demo_token.balanceOf(accounts[0])
    }

    # approve token in
    test_token.approve(pair_address, swap_amount, {"from": accounts[0]})
    # swap
    pair.swap(test_token.address, swap_amount, {"from": accounts[0]})

    balances = {
        "test_token": test_token.balanceOf(accounts[0]),
        "demo_token": demo_token.balanceOf(accounts[0])
    }

    # test that we swapped tokens correctly
    assert initial_balances["demo_token"] < balances["demo_token"]
    assert initial_balances["test_token"] > balances["test_token"]

    # get shares
    shares = pair.balanceOf(accounts[0])

    # remove liquidity
    pair.removeLiquidity(shares, {"from": accounts[0]})

    assert pair.balanceOf(accounts[0]) == 0 # minted shares
    assert pair.reserve0() == 0
    assert pair.reserve1() == 0
    assert test_token.balanceOf(pair) == 0
    assert demo_token.balanceOf(pair) == 0

    balances = {
        "test_token": test_token.balanceOf(accounts[0]),
        "demo_token": demo_token.balanceOf(accounts[0])
    }

    # test that we swapped tokens correctly
    assert initial_balances["demo_token"] < balances["demo_token"]
    assert initial_balances["test_token"] < balances["test_token"]
Enter fullscreen mode Exit fullscreen mode

To run our tests, on your terminal run the following command:

$ brownie test --network hardhat
Enter fullscreen mode Exit fullscreen mode

This will test our smart contracts on the hardhat local node.

That would be it, we can extend our testing to include every possible scenario, but we will not cover that in this tutorial.

Deploy using Brownie

In this tutorial, we are going to use Brownie to deploy our smart contracts, you can use any other smart contract framework if you are familiar with it Hardhat or ApeWorx both have support for developing and deploying vyper smart contracts.

Since we are deploying to Avalanche, we need to add the Avalanche mainnet and the Avalanche Fuji testnet to our supported networks. In our terminal type the following:

$ brownie networks add Avalanche avax-testnet host=https://api.avax-test.network/ext/bc/C/rpc chainid=43113 explorer=https://testnet.snowtrace.io/ name=Fuji
Enter fullscreen mode Exit fullscreen mode
$ brownie networks add Avalanche avax-mainnet host=https://api.avax.network/ext/bc/C/rpc chainid=43114 explorer=https://snowtrace.io/ name=Mainnet
Enter fullscreen mode Exit fullscreen mode

After we run these two commands brownie will add Avalanche to Brownie.

Avalanche
  โ”œโ”€Mainnet: avax-mainnet
  โ””โ”€Fuji: avax-testnet
Enter fullscreen mode Exit fullscreen mode

After that is complete, you can write our deploy script.

Our script is really simple we are going to take our two smart contracts, and deploy them both, first, we need to deploy the pair smart contract since the factory smart contract needs a reference to that one.

Our script will first check if we are on any of the Avalanche networks, and if so will deploy the smart contracts using a real wallet (with some AVAX in it), if not we will assume that we are on our local network and deploy them there.

To deploy to a live blockchain (Fuji or Mainnet) we need to have a wallet with some AVAX on it, you can get some testnet AVAX tokens on this faucet. Also, we need to define our brownie-config.yaml file to let Brownie know where to find our private key, and some important configurations as well.

Create a brownie-config.yaml on the root of your project. It should look something like this.

dotenv: .env

networks:
  default: hardhat

wallets:
  from_key: ${PRIVATE_KEY}

compiler:
  vyper:
    version: "0.3.7"
Enter fullscreen mode Exit fullscreen mode

The configuration file is expecting a .env file to work properly, create one on the root of your project and populate it with this.

PRIVATE_KEY="YOUR_PRIVATE_KEY_HERE"
Enter fullscreen mode Exit fullscreen mode

After we have our configuration setup, we need to write our deploy script. On the scripts folder create a new file called deploy.py this python program will be responsible for deploying our smart contracts.

The deploy.py file should look something like this:

from brownie import accounts, network, config, AvaSwapFactory, AvaSwapPair


def main():
    supported_networks = ["avax-mainnet", "avax-testnet"]
    active_network = network.show_active()

    if active_network in supported_networks:
        deployer = accounts.add(config["wallets"]["from_key"])
    else:
        deployer = accounts[0]

    ava_swap_pair = AvaSwapPair.deploy({"from": deployer}) 
    ava_swap_factory = AvaSwapFactory.deploy(ava_swap_pair.address, {"from": deployer})
Enter fullscreen mode Exit fullscreen mode

To run this script write the following command on your terminal, this will deploy the contract to your hardhat local node.

$ brownie run deploy
Enter fullscreen mode Exit fullscreen mode

To deploy it to a testnet write the following one.

$ brownie run deploy --network avax-testnet
Enter fullscreen mode Exit fullscreen mode

And to deploy them to the Avalanche mainnet run the following command:

$ brownie run deploy --network avax-mainnet
Enter fullscreen mode Exit fullscreen mode

And that would be it on the smart contract side, we created, tested, and deployed our AMM smart contracts.

Conclusion

Automatic Market Makers are a wonderful tool used in the decentralized finance space, it allows users to swap tokens without "normal" market makers almost instantly. These kinds of smart contracts have a lot of use cases, and you can expand a lot more on this automatic market maker idea. Be creative!

Top comments (0)