An constant product market maker (automated market maker) that accepts a market inventory and a user trade, and returns the updated market and user balances. The constant product swap formula enforces that the product of two inventory items (item a and b) are the same pre- and post-trade. This automatically increases prices for scarce items and lowers prices of plentiful items.

market_a_post * market_b_post = market_a_pre * market_b_pre

A user gives a and receives b. The purpose of the formula is to find how much b the user receives.

market_a_post = market_a_pre + user_gives_a  # User gives.
market_b_post = market_b_pre - user_gets_b  # User takes.

# Substitute into original
(market_a_pre + user_gives_a) * (market_b_pre - user_gets_b) =  market_a_pre * market_b_pre

# Expand, note that the right hand term is cancelled. Simplify:
user_gets_b = market_b_pre * user_gives_a / (market_a_pre + user_gives_a)

Building a stripped-down version of this contract.

%lang starknet
%builtins range_check

from starkware.cairo.common.math import assert_nn_le, unsigned_div_rem

# The maximum value an item can have.
const BALANCE_UPPER_BOUND = 2 ** 64

# Accepts an AMM state and an order, instantiates AMM, swaps, returns balances.
# The market gains item `a` loses item `b`, the user loses item `a` gains item `b`.
@external
func trade{range_check_ptr}(
    market_a_pre : felt, market_b_pre : felt, user_gives_a : felt) -> (
    market_a_post : felt, market_b_post : felt, user_gets_b : felt):
    # Prevent values exceeding max.
    assert_nn_le(market_a_pre, BALANCE_UPPER_BOUND - 1)
    assert_nn_le(market_b_pre, BALANCE_UPPER_BOUND - 1)
    assert_nn_le(user_gives_a, BALANCE_UPPER_BOUND - 1)

    # Calculated how much item `b` the user gets.
    # user_gets_b = market_b_pre * user_gives_a // (market_a_pre + user_gives_a)
    let (user_gets_b, _) = unsigned_div_rem(
        market_b_pre * user_gives_a, market_a_pre + user_gives_a)

    # Calculate what the market is left with.
    let market_a_post = market_a_pre + user_gives_a
    let market_b_post = market_b_pre - user_gets_b

    # Ensure that all value updates are >= 1.
    assert_nn_le(1, user_gives_a)
    assert_nn_le(1, user_gets_b)
    assert_nn_le(1, market_a_post - market_a_pre)
    assert_nn_le(1, market_b_pre - market_b_post)

    # Check not items conjured into existence.
    assert (market_a_pre + market_b_pre + user_gives_a) = (
        market_a_post + market_b_post + user_gets_b)
    return (market_a_post, market_b_post, user_gets_b)
end

Save as MarketMaker.cairo.

Compile

Then, to compile:

starknet-compile MarketMaker.cairo \
    --output MarketMaker_compiled.json \
    --abi MarketMaker_contract_abi.json

Test

Make a new file called MarketMaker_contract_test.py and populate it:

import os
import pytest

from starkware.starknet.compiler.compile import (
    compile_starknet_files)
from starkware.starknet.testing.starknet import Starknet
from starkware.starknet.testing.contract import StarknetContract

# The path to the contract source code.
CONTRACT_FILE = os.path.join(
    os.path.dirname(__file__), "MarketMaker.cairo")


# The testing library uses python's asyncio. So the following
# decorator and the ``async`` keyword are needed.
@pytest.mark.asyncio
async def test_record_items():
    # Compile the contract.
    contract_definition = compile_starknet_files(
        [CONTRACT_FILE], debug_info=True)

    # Create a new Starknet class that simulates the StarkNet
    # system.
    starknet = await Starknet.empty()

    # Deploy the contract.
    contract_address = await starknet.deploy(
        contract_definition=contract_definition)
    contract = StarknetContract(
        starknet=starknet,
        abi=contract_definition.abi,
        contract_address=contract_address,
    )

    market_a_pre = 300
    market_b_pre = 500
    user_a_pre = 40  # User gives 40.
    res = await contract.trade(market_a_pre, market_b_pre, user_a_pre).invoke()
    (market_a_post, market_b_post, user_b_post, ) = res

    assert market_a_post == market_a_pre + user_a_pre
    assert market_b_post == market_b_pre - user_b_post

The packages required for testing are installed by default with cairo-lang. Run the test:

pytest MarketMaker_contract_test.py.

Check that the tests passed:

=== 1 passed, 1 warning in 11.26s ===

Deploy

Then, to deploy:

starknet deploy --contract MarketMaker_compiled.json \
    --network=alpha

Returns:
Deploy transaction was sent.
Contract address: 0x07f9ad51033cd6107ad7d70d01c3b0ba2dda3331163a45b6b7f1a2952dac0880
Transaction ID: 133739

Monitor

Check the status of the transaction:

starknet tx_status --network=alpha --id=133739

Returns:
{
    "block_id": 14520,
    "tx_status": "PENDING"
}

The block and the contract

Interact

Trades aren’t stored in this contract so we can use call (view) instead of invoke (write)

Create a trade for a market with 500 A, 1000 B and a user who is giving 30 A.

starknet call \
    --network=alpha \
    --address 0x07f9ad51033cd6107ad7d70d01c3b0ba2dda3331163a45b6b7f1a2952dac0880 \
    --abi MarketMaker_contract_abi.json \
    --function trade \
    --inputs 500 1000 30

Returns:
530 944 56

The market gained 30 A and the user gained 56 B.

Now try with the pairs reversed. Create a trade for a market with 1000 A, 500 B and a user who is giving 30 A.

starknet call \
    --network=alpha \
    --address 0x07f9ad51033cd6107ad7d70d01c3b0ba2dda3331163a45b6b7f1a2952dac0880 \
    --abi MarketMaker_contract_abi.json \
    --function trade \
    --inputs 1000 500 30

Returns:
1030 486 14

The market gained 30 A and the user gained 14 B. The user was giving an item that the market has plenty of, and you can see that they received a smaller quantity than in the first example.

This contract may now be called by the GameEngineV1.cairo contract.