# A simple Automated Market Maker (AMM)
- Starknet Guide

By [Rocco111](https://paragraph.com/@rocco111) · 2023-01-25

---

In this tutorial, we’ll review the code of a simple AMM, written as a StarkNet contract, highlighting specific implementation details. The contract is deployable (and is actually deployed – [go check it out](https://amm-demo.starknet.starkware.co/)) to the StarkNet Alpha release, and will be seamlessly deployable and compatible with future StarkNet releases.

We will start by describing the scope of the contract functionality, and after that, we will dive into the implementation. Finally, we’ll show how to invoke the demo contract’s functionality on the StarkNet Alpha environment with a few concrete examples.

Before we begin, you can review the full contract code [here](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/apps/amm_sample/amm_sample.cairo).

AMM implementation in StarkNet Alpha
------------------------------------

In order to understand the basics of automated market making, you may refer to the [Uniswap docs](https://uniswap.org/docs/v2/protocol-overview/how-uniswap-works/), or check the short description in our previous [AMM tutorial](https://starknet.io/docs/hello_cairo/amm.html#amm-cairo#amm-cairo). For those who read the previous tutorial – comparing the code written there to the contract code in this tutorial can be a fun exercise that highlights the power of StarkNet

In this sample contract we’ll limit our functionality to exactly one pool to be managed by the contract. We will implement a straightforward swap functionality (in both directions), using a simple curve; i.e. the constant product formula (x \* y = k).  We will refer to the tokens managed by the AMM as token A and token B, which may play the role of any type of fungible tokens.

Some aspects that ideally would’ve been implemented in other contracts, e.g. minting tokens in an ERC20 contract, are mocked in this sample contract for simplicity. This functionality is not inherent to AMM functionality.

What’s important to learn from this example is how StarkNet allows the developer of the application to focus on specifying their verifiable business logic and constraints, all while enjoying massive scalability without compromising security. In other words, only the invocable functions and the relevant storage variables used to maintain the state of the application need to be specified by the developer.

The AMM state
-------------

Let’s dive into the implementation. We’ll start by reviewing how we maintain the state of the AMM.

We require two dedicated fields in order to maintain the state:

1.  The pool balance – how much liquidity is available in the pool, per token.
    
2.  The account balances – how many tokens of each type are kept in each account. As explained above, this is only needed for this release, and will be replaced with regular ERC-20 interactions in the future.
    

In StarkNet, the programmatic model for storage is a simple key/value store. We can define a [storage variable](https://docs.starknet.io/documentation/getting_started/intro/#storage-var), so reading from and writing to storage is simply a matter of calling read and write on that variable.

For the pool balance we define:

**@storage\_var**

**func pool\_balance(token\_type: felt) -> (balance: felt) {**

**}**

The pool balance is defined as a mapping between the token type (predefined constants) and the balance available in the pool for that token type.

For the account balances we define:

**@storage\_var**

**func account\_balance(account\_id: felt, token\_type: felt) -> (**

**balance: felt**

**) {**

**}**

The account balance is defined as a mapping from the account id and token type to the balance available in that account, for the given token type.

We write a function that allows us to _modify_ the balance of a given token type in a given account:

**func modify\_account\_balance{**

**syscall\_ptr: felt\*,**

**pedersen\_ptr: HashBuiltin\*,**

**range\_check\_ptr,**

**}(account\_id: felt, token\_type: felt, amount: felt) {**

**let (current\_balance) = account\_balance.read(**

**account\_id, token\_type**

**);**

**tempvar new\_balance = current\_balance + amount;**

**assert\_nn\_le(new\_balance, BALANCE\_UPPER\_BOUND - 1);**

**account\_balance.write(**

**account\_id=account\_id,**

**token\_type=token\_type,**

**value=new\_balance,**

**);**

**return ();**

**}**

The logic is fairly straightforward:

*   Retrieve the existing account balance.
    
*   Calculate the new balance.
    
*   Assert it is not negative and doesn’t exceed the upper bound.
    
*   Write it to the account balance storage variable.
    

Note that this also covers cases where we subtract an amount from the balance.

As mentioned before, we assume that the reader is familiar with Cairo syntax. For those who are not, we briefly mention the relevant concepts.

First, we observe the usage of [implicit arguments](https://starknet.io/docs/how_cairo_works/builtins.html#implicit-arguments) passed to this function inside the curly brackets. Specifically, the arguments necessary for the assertion and storage operations. Wherever such functionality is used, we’ll pass these implicit arguments.

Next, the assert functions used here are imported from Cairo’s [common math library](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/cairo/common/math.cairo) . In this case, assert\_nn\_le asserts that the first argument is nonnegative and is less than or equal to the second argument.

To allow a user to read the balance of an account, we define the following [view function](https://docs.starknet.io/documentation/getting_started/intro/#view-decorator):

**@view**

**func get\_account\_token\_balance{**

**syscall\_ptr: felt\*,**

**pedersen\_ptr: HashBuiltin\*,**

**range\_check\_ptr,**

**}(account\_id: felt, token\_type: felt) -> (balance: felt) {**

**return account\_balance.read(account\_id, token\_type);**

**}**

**Similarly, for the pool balance:**

**func set\_pool\_token\_balance{**

**syscall\_ptr: felt\*,**

**pedersen\_ptr: HashBuiltin\*,**

**range\_check\_ptr,**

**}(token\_type: felt, balance: felt) {**

**assert\_nn\_le(balance, BALANCE\_UPPER\_BOUND - 1);**

**pool\_balance.write(token\_type, balance);**

**return ();**

**}**

**@view**

**func get\_pool\_token\_balance{**

**syscall\_ptr: felt\*,**

**pedersen\_ptr: HashBuiltin\*,**

**range\_check\_ptr,**

**}(token\_type: felt) -> (balance: felt) {**

**return pool\_balance.read(token\_type);**

**}**

Swapping tokens
---------------

We now proceed to the primary functionality of the contract – swapping tokens.

**@external**

**func swap{**

**syscall\_ptr: felt\*,**

**pedersen\_ptr: HashBuiltin\*,**

**range\_check\_ptr,**

**}(token\_from: felt, amount\_from: felt) -> (amount\_to: felt) {**

**let (account\_id) = get\_caller\_address();**

\*\*    _// Verify that token\_from is either TOKEN\_TYPE\_A or TOKEN\_TYPE\_B._\*\*

**assert (token\_from - TOKEN\_TYPE\_A) \* (token\_from - TOKEN\_TYPE\_B) = 0;**

\*\*    _// Check that the requested amount\_from is valid._\*\*

**assert\_nn\_le(amount\_from, BALANCE\_UPPER\_BOUND - 1);**

\*\*    _// Check that the user has enough funds._\*\*

**let (account\_from\_balance) = get\_account\_token\_balance(**

**account\_id=account\_id, token\_type=token\_from**

**);**

**assert\_le(amount\_from, account\_from\_balance);**

\*\*    _// Execute the actual swap._\*\*

**let (token\_to) = get\_opposite\_token(token\_type=token\_from);**

**let (amount\_to) = do\_swap(**

**account\_id=account\_id,**

**token\_from=token\_from,**

**token\_to=token\_to,**

**amount\_from=amount\_from,**

**);**

**return (amount\_to=amount\_to);**

**}**

swap receives as inputs the account id, the token type and an amount of the token to be swapped. The function starts by verifying the validity of the inputs:

*   The token type is a valid token, by asserting that it is equal to one of the pool’s token types.
    
*   The amount requested to be swapped is valid – it does not exceed the upper bound, and the account has enough funds to swap.
    

If all checks pass, we proceed to execute the swap.

**func get\_opposite\_token(token\_type: felt) -> (t: felt) {**

**if (token\_type == TOKEN\_TYPE\_A) {**

**return (t=TOKEN\_TYPE\_B);**

**} else {**

**return (t=TOKEN\_TYPE\_A);**

**}**

**}**

get\_opposite\_token receives as input a token type and returns the opposite token type.

**func do\_swap{**

**syscall\_ptr: felt\*,**

**pedersen\_ptr: HashBuiltin\*,**

**range\_check\_ptr,**

**}(**

**account\_id: felt,**

**token\_from: felt,**

**token\_to: felt,**

**amount\_from: felt,**

**) -> (amount\_to: felt) {**

**alloc\_locals;**

\*\*    _// Get pool balance._\*\*

**let (local amm\_from\_balance) = get\_pool\_token\_balance(**

**token\_type=token\_from**

**);**

**let (local amm\_to\_balance) = get\_pool\_token\_balance(**

**token\_type=token\_to**

**);**

\*\*    _// Calculate swap amount._\*\*

**let (local amount\_to, \_) = unsigned\_div\_rem(**

**amm\_to\_balance \* amount\_from,**

**amm\_from\_balance + amount\_from,**

**);**

\*\*    _// Update token\_from balances._\*\*

**modify\_account\_balance(**

**account\_id=account\_id,**

**token\_type=token\_from,**

**amount=-amount\_from,**

**);**

**set\_pool\_token\_balance(**

**token\_type=token\_from,**

**balance=amm\_from\_balance + amount\_from,**

**);**

\*\*    _// Update token\_to balances._\*\*

**modify\_account\_balance(**

**account\_id=account\_id,**

**token\_type=token\_to,**

**amount=amount\_to,**

**);**

**set\_pool\_token\_balance(**

**token\_type=token\_to, balance=amm\_to\_balance - amount\_to**

**);**

**return (amount\_to=amount\_to);**

**}**

The logic of the swapping itself is fairly straightforward:

1.  Retrieve the amount of tokens available in the pool, per token type.
    
2.  Calculate the amount of tokens of the opposite type to be received by the pool.
    
3.  Update the account balances for both tokens, as well as the pool’s balances.
    

Most of this implementation invokes functions we described earlier (get\_pool\_token\_balance, modify\_account\_balance, set\_pool\_token\_balance). Note that the calculation of the amount to be swapped essentially implements the AMM constant product formula:

![](https://storage.googleapis.com/papyrus_images/47ec7e252d9e19ec12dff81b32b8127117a3ee7cce85bf53206350565b2594fc.png)

We use Cairo’s common math library, specifically unsigned\_div\_rem (unsigned division with remainder) to calculate the amount of tokens to be received.

Initialising the AMM
--------------------

As we don’t have contract interaction and liquidity providers in this version, we will now define how to initialize the AMM – both the liquidity pool itself and some account balances.

**@external**

**func init\_pool{**

**syscall\_ptr: felt\*,**

**pedersen\_ptr: HashBuiltin\*,**

**range\_check\_ptr,**

**}(token\_a: felt, token\_b: felt) {**

**assert\_nn\_le(token\_a, POOL\_UPPER\_BOUND - 1);**

**assert\_nn\_le(token\_b, POOL\_UPPER\_BOUND - 1);**

**set\_pool\_token\_balance(token\_type=TOKEN\_TYPE\_A, bal=token\_a);**

**set\_pool\_token\_balance(token\_type=TOKEN\_TYPE\_B, bal=token\_b);**

**return ();**

**}**

Initializing the pool is a simple function that accepts two balances for the tokens (A,B), and sets them using the set\_pool\_token\_balance function we defined above: The POOL\_UPPER\_BOUND is a constant defined to prevent overflows.

Having this function defined, we proceed to add demo tokens to an account:

**@external**

**func add\_demo\_token{**

**syscall\_ptr: felt\*,**

**pedersen\_ptr: HashBuiltin\*,**

**range\_check\_ptr,**

**}(token\_a\_amount: felt, token\_b\_amount: felt) {**

**let (account\_id) = get\_caller\_address();**

\*\*    _// Make sure the account's balance is much smaller than_\*\*

\*\*    _// the pool init balance._\*\*

**assert\_nn\_le(token\_a\_amount, ACCOUNT\_BALANCE\_BOUND - 1);**

**assert\_nn\_le(token\_b\_amount, ACCOUNT\_BALANCE\_BOUND - 1);**

**modify\_account\_balance(**

**account\_id=account\_id,**

**token\_type=TOKEN\_TYPE\_A,**

**amount=token\_a\_amount,**

**);**

**modify\_account\_balance(**

**account\_id=account\_id,**

**token\_type=TOKEN\_TYPE\_B,**

**amount=token\_b\_amount,**

**);**

**return ();**

**}**

Note that here we add another business constraint (for demo purposes) that the account is capped at some number calculated as a ratio from the pool cap. Specifically, ACCOUNT\_BALANCE\_BOUND is defined as POOL\_UPPER\_BOUND divided by 1000, so the cap for an account is 1/1000 that of a pool. All constants are defined at the top of the contract file.

Interaction examples
--------------------

We can now explore a few examples that demonstrate contract interaction using the StarkNet CLI.

Set the environment variable STARKNET\_NETWORK as follows:

export STARKNET\_NETWORK=alpha-goerli

For this section you need the [amm\_sample.cairo](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/apps/amm_sample/amm_sample.cairo) contract code.

To generate the ABI of the contract, enter the following commands:

starknet-compile amm\_sample.cairo \\

\--output amm\_sample\_compiled.json \\

\--abi amm\_sample\_abi.json

First, declare and deploy the contract as explained in [Declare the contract on the StarkNet testnet](https://docs.starknet.io/documentation/getting_started/intro/#declare-the-contract-on-the-starknet-testnet) and [Deploy the contract on the StarkNet testnet](https://docs.starknet.io/documentation/getting_started/intro/#deploy-the-contract-on-the-starknet-testnet). Denote the new deployed contract address by ${AMM\_ADDRESS}.

We assume you are familiar with the StarkNet CLI. If this is not the case, we recommend you review [this section](https://docs.starknet.io/documentation/getting_started/intro/).

Query the pool’s balance using:

**starknet call \\**

**\--address ${AMM\_ADDRESS} \\**

**\--abi amm\_sample\_abi.json \\**

**\--function get\_pool\_token\_balance \\**

**\--inputs 1**

In response, you should get the pool’s balance of token 1.

Now let’s add some tokens to our account’s balance. (Note that every interaction with a contract through a function invocation must be done using an account. To set up an account, see [Setting up a StarkNet account](https://docs.starknet.io/documentation/getting_started/account_setup/).)

**starknet invoke \\**

**\--address ${AMM\_ADDRESS} \\**

**\--abi amm\_sample\_abi.json \\**

**\--function add\_demo\_token \\**

**\--inputs 1000 1000**

Now that we have some tokens, we can use the AMM and swap 500 units of token 1 in exchange for some units of token 2 (the exact number depends on the current balance of the pool).

**starknet invoke \\**

**\--address ${AMM\_ADDRESS} \\**

**\--abi amm\_sample\_abi.json \\**

**\--function swap \\**

**\--inputs 1 500**

You can now query the account’s balance of token 2 after the swap (replace ${ACCOUNT\_ADDRESS} with your account address):

**starknet call \\**

**\--address ${AMM\_ADDRESS} \\**

**\--abi amm\_sample\_abi.json \\**

**\--function get\_account\_token\_balance \\**

**\--inputs ${ACCOUNT\_ADDRESS} 2**

Note that the change will only take effect after the swap transaction’s status is either ACCEPTED\_ON\_L2 or ACCEPTED\_ON\_L1.

---

*Originally published on [Rocco111](https://paragraph.com/@rocco111/a-simple-automated-market-maker-amm-starknet-guide)*
