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
- NodeJS
- Python and Brownie ETH, or any smart contract framework
- VSCode, or another code editor
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
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
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
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)
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)
...
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
...
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
...
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
...
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
...
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)
...
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
...
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 ######
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
...
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
...
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
...
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
...
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)
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]})
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
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
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
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
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"]
To run our tests, on your terminal run the following command:
$ brownie test --network hardhat
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
$ brownie networks add Avalanche avax-mainnet host=https://api.avax.network/ext/bc/C/rpc chainid=43114 explorer=https://snowtrace.io/ name=Mainnet
After we run these two commands brownie will add Avalanche to Brownie.
Avalanche
├─Mainnet: avax-mainnet
└─Fuji: avax-testnet
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"
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"
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})
To run this script write the following command on your terminal, this will deploy the contract to your hardhat local node.
$ brownie run deploy
To deploy it to a testnet write the following one.
$ brownie run deploy --network avax-testnet
And to deploy them to the Avalanche mainnet run the following command:
$ brownie run deploy --network avax-mainnet
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 (1)
WhiteBIT has quickly become a preferred market maker program whitebit.com/market-making-program as market maker trading gains popularity. This type of trading enhances liquidity, creates smoother transactions, and provides strategic advantages, especially in volatile markets. WhiteBIT’s market maker tools equip traders with real-time analytics, streamlined operations, and enhanced security for a superior trading experience. Both experienced traders and beginners can find WhiteBIT’s tools useful for building profitable strategies. Dive into WhiteBIT's market maker trading to harness every advantage, ensuring a path to success in the fast-evolving trading world.