Source Code

;; traits
(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)


;; constants
;; 
(define-constant MINIMUM_LIQUIDITY u1000) ;; minimum liquidity that must exist in a pool
(define-constant THIS_CONTRACT (as-contract tx-sender)) ;; this contract
(define-constant FEES_DENOM u10000) ;; fees denominator

;; errors
(define-constant ERR_POOL_ALREADY_EXISTS (err u200)) ;; pool already exists
(define-constant ERR_INCORRECT_TOKEN_ORDERING (err u201)) ;; incorrect token ordering (invalid sorting)
(define-constant ERR_INSUFFICIENT_LIQUIDITY_MINTED (err u202)) ;; insufficient liquidity amounts being added
(define-constant ERR_INSUFFICIENT_LIQUIDITY_OWNED (err u203)) ;; not enough liquidity owned to withdraw the requested amount
(define-constant ERR_INSUFFICIENT_LIQUIDITY_BURNED (err u204)) ;; insufficient liquidity amounts being removed
(define-constant ERR_INSUFFICIENT_INPUT_AMOUNT (err u205)) ;; insufficient input token amount for swap
(define-constant ERR_INSUFFICIENT_LIQUIDITY_FOR_SWAP (err u206)) ;; insufficient liquidity in pool for swap
(define-constant ERR_INSUFFICIENT_1_AMOUNT (err u207)) ;; insufficient amount of token 1 for swap
(define-constant ERR_INSUFFICIENT_0_AMOUNT (err u208)) ;; insufficient amount of token 0 for swap

;; mappings
(define-map pools
    (buff 20) ;; Pool ID (hash of Token0 + Token1 + Fee)
    { 
        token-0: principal,
        token-1: principal,
        fee: uint,

        liquidity: uint,
        balance-0: uint,
        balance-1: uint
    }
)

(define-map positions 
    { 
        pool-id: (buff 20),
        owner: principal
    } 
    { 
        liquidity: uint
    }
)


;; create-pool
;; Creates a new pool with the given token-0, token-1, and fee
;; Ensures that a pool with these two tokens and given fee amount does not already exist
(define-public (create-pool (token-0 <ft-trait>) (token-1 <ft-trait>) (fee uint)) 
    (let (
        ;; Create a pool-info tuple with the information
        (pool-info {
            token-0: token-0,
            token-1: token-1,
            fee: fee
        })
        ;; Compute the pool ID
        (pool-id (get-pool-id pool-info))
        ;; Ensure this Pool ID doesn't already exist in the `pools` map
        (pool-does-not-exist (is-none (map-get? pools pool-id)))

        ;; Convert the <ft-trait> values into principals
        (token-0-principal (contract-of token-0))
        (token-1-principal (contract-of token-1))

        ;; Prepare the pool-data tuple
        (pool-data {
            token-0: token-0-principal,
            token-1: token-1-principal,
            fee: (get fee pool-info),
            liquidity: u0, ;; initially, liquidity is 0
            balance-0: u0, ;; initially, balance-0 (x) is 0
            balance-1: u0 ;; initially, balance-1 (y) is 0
        })
    ) 

    ;; If pool does already exist, throw an error
    (asserts! pool-does-not-exist ERR_POOL_ALREADY_EXISTS)
    ;; If the token-0 principal is not "less than" the token-1 principal, throw an error
    (asserts! (is-ok (correct-token-ordering token-0-principal token-1-principal)) ERR_INCORRECT_TOKEN_ORDERING)
    
    ;; Update the `pools` map with the new pool data
    (map-set pools pool-id pool-data)
    (print { action: "create-pool", data: pool-data})
    (ok true)
    )
)


;; add-liquidity
;; Adds liquidity to a given pool
;; Ensure the pool exists, calculate what token amounts are possible to add as liquidity, handle the case where this is the first time liquidity is being added
;; Transfer tokens from user to pool, and update mappings as needed
(define-public (add-liquidity (token-0 <ft-trait>) (token-1 <ft-trait>) (fee uint) (amount-0-desired uint) (amount-1-desired uint) (amount-0-min uint) (amount-1-min uint))
    (let
        (
            ;; compute the pool id and fetch the current state of the pool from the mapping
            (pool-info {
                token-0: token-0,
                token-1: token-1,
                fee: fee
            })
            (pool-id (get-pool-id pool-info))
            (pool-data (unwrap! (map-get? pools pool-id) (err u0)))
            (sender tx-sender)
            
            (pool-liquidity (get liquidity pool-data))
            (balance-0 (get balance-0 pool-data))
            (balance-1 (get balance-1 pool-data))

            ;; fetch the current liquidity of the user in the pool (default 0 if no existing position)
            (user-liquidity (unwrap! (get-position-liquidity pool-id sender) (err u0)))

            ;; is this the first time liquidity is being added?
            (is-initial-liquidity (is-eq pool-liquidity u0))
            (amounts 
                (if 
                    is-initial-liquidity
                    ;; if it is the first time, we can add tokens in whatever amounts we want
                    {amount-0: amount-0-desired, amount-1: amount-1-desired}
                    ;; otherwise, we use get-amounts to calculate the amounts of tokens to add within the constraints
                    (unwrap! (get-amounts amount-0-desired amount-1-desired amount-0-min amount-1-min balance-0 balance-1) (err u0))
                )
            )
            (amount-0 (get amount-0 amounts))
            (amount-1 (get amount-1 amounts))
            ;; calculate the new liquidity (L value) 
            (new-liquidity 
                (if 
                    is-initial-liquidity

                    ;; if this is first-time liquidity, we subtract MINIMUM_LIQUIDITY to make sure that the pool has at least some liquidity forever
                    ;; so we compute L = sqrt(x * y) - MINIMUM_LIQUIDITY
                    (- (sqrti (* amount-0 amount-1)) MINIMUM_LIQUIDITY)

                    ;; if it is not the first time, we update L based on how much % of the pool's liquidity this user is adding
                    ;; min( amount0 * pool-liquidity / balance0 , amount1 * pool-liquidity / balance1 )
                    (min (/ (* amount-0 pool-liquidity) balance-0) (/ (* amount-1 pool-liquidity) balance-1))
                )
            )
            (new-pool-liquidity
                (if
                    is-initial-liquidity
                    (+ new-liquidity MINIMUM_LIQUIDITY)
                    new-liquidity
                )
            )
        )
        (asserts! (> new-liquidity u0) ERR_INSUFFICIENT_LIQUIDITY_MINTED)

        ;; transfer tokens from user to pool
        (try! (contract-call? token-0 transfer amount-0 sender THIS_CONTRACT none))
        (try! (contract-call? token-1 transfer amount-1 sender THIS_CONTRACT none))

        ;; update the `positions` map with the new liquidity of the user (pre existing liquidity + new liquidity)
        (map-set positions 
            {
                pool-id: pool-id,
                owner: sender
            }
            {
                liquidity: (+ user-liquidity new-liquidity)
            }
        )

        ;; update the `pools` map with the new pool liquidity, balance-0, and balance-1
        (map-set pools pool-id (merge pool-data {
            liquidity: (+ pool-liquidity new-pool-liquidity),
            balance-0: (+ balance-0 amount-0),
            balance-1: (+ balance-1 amount-1)
        }))

        (print { action: "add-liquidity", pool-id: pool-id, amount-0: amount-0, amount-1: amount-1, liquidity: (+ user-liquidity new-liquidity) })
        (ok true)
    )
)

;; remove-liquidity
;; Removes liquidity from a given pool
;; Ensure the pool exists, ensures the user owns enough liquidity as they want to remove, calculate amount of tokens to give back to them
;; Transfer tokens from pool to user, and update mappings as needed
(define-public (remove-liquidity (token-0 <ft-trait>) (token-1 <ft-trait>) (fee uint) (liquidity uint))
    (let
        (
            ;; compute the pool id and fetch the current state of the pool from the mapping
            (pool-info {
                token-0: token-0,
                token-1: token-1,
                fee: fee
            })
            (pool-id (get-pool-id pool-info))
            (pool-data (unwrap! (map-get? pools pool-id) (err u0)))
            (sender tx-sender)

            (pool-liquidity (get liquidity pool-data))
            (balance-0 (get balance-0 pool-data))
            (balance-1 (get balance-1 pool-data))

            ;; fetch the user's position
            (user-liquidity (unwrap! (get-position-liquidity pool-id sender) (err u0)))

            ;; calculate how much amount-0 and amount-1 the user should receive as a % of how much they are withdrawing compared to the total pool liquidity
            (amount-0 (/ (* liquidity balance-0) pool-liquidity))
            (amount-1 (/ (* liquidity balance-1) pool-liquidity))

        )

        ;; make sure user owns enough liquidity to withdraw
        (asserts! (>= user-liquidity liquidity) ERR_INSUFFICIENT_LIQUIDITY_OWNED)
        ;; make sure user is getting at least some amount of tokens back
        (asserts! (> amount-0 u0) ERR_INSUFFICIENT_LIQUIDITY_BURNED)
        (asserts! (> amount-1 u0) ERR_INSUFFICIENT_LIQUIDITY_BURNED)

        ;; transfer tokens from pool to user
        (try! (as-contract (contract-call? token-0 transfer amount-0 THIS_CONTRACT sender none)))
        (try! (as-contract (contract-call? token-1 transfer amount-1 THIS_CONTRACT sender none)))

        ;; update the `positions` map with the new liquidity of the user (pre existing liquidity - new liquidity)
        (map-set positions 
            {
                pool-id: pool-id,
                owner: sender
            }
            {
                liquidity: (- user-liquidity liquidity)
            }
        )

        ;; update the `pools` map with the new pool liquidity, balance-0, and balance-1
        (map-set pools pool-id (merge pool-data {
            liquidity: (- pool-liquidity liquidity),
            balance-0: (- balance-0 amount-0),
            balance-1: (- balance-1 amount-1)
        }))
        (print { action: "remove-liquidity", pool-id: pool-id, amount-0: amount-0, amount-1: amount-1, liquidity: liquidity })
        (ok true)
    )
)


;; swap
;; Swaps two tokens in a given pool
;; Ensure the pool exists, calculate the amount of tokens to give back to the user, handle the case where the user is swapping for token-0 or token-1
;; Transfer input token from user to pool, transfer output token from pool to user, and update mappings as needed
(define-public (swap (token-0 <ft-trait>) (token-1 <ft-trait>) (fee uint) (input-amount uint) (zero-for-one bool)) 
    (let
        (
            ;; compute the pool id and fetch the current state of the pool from the mapping
            (pool-info {
                token-0: token-0,
                token-1: token-1,
                fee: fee
            })
            (pool-id (get-pool-id pool-info))
            (pool-data (unwrap! (map-get? pools pool-id) (err u0)))
            (sender tx-sender)

            (pool-liquidity (get liquidity pool-data))
            (balance-0 (get balance-0 pool-data))
            (balance-1 (get balance-1 pool-data))

            ;; xy = k
            ;; compute the constant k before the swap
            (k (* balance-0 balance-1))

            ;; keep track of which token is the input and which is the output
            ;; based on the value of zero-for-one
            (input-token (if zero-for-one token-0 token-1))
            (output-token (if zero-for-one token-1 token-0))

            ;; keep track of the input and output balances
            (input-balance (if zero-for-one balance-0 balance-1))
            (output-balance (if zero-for-one balance-1 balance-0))

            ;; compute the output amount by solving xy = k
            (output-amount (- output-balance (/ k (+ input-balance input-amount))))
            ;; calculate fees to charge as a % of the output amount
            (fees (/ (* output-amount fee) FEES_DENOM))
            ;; subtract the fees from the output amount
            (output-amount-sub-fees (- output-amount fees))

            ;; compute the new balances of the pool after the swap
            (balance-0-post-swap (if zero-for-one (+ balance-0 input-amount) (- balance-0 output-amount-sub-fees)))
            (balance-1-post-swap (if zero-for-one (- balance-1 output-amount-sub-fees) (+ balance-1 input-amount)))
        )

        ;; make sure user is swapping >0 tokens
        (asserts! (> input-amount u0) ERR_INSUFFICIENT_INPUT_AMOUNT)
        ;; make sure user is getting back >0 tokens
        (asserts! (> output-amount-sub-fees u0) ERR_INSUFFICIENT_LIQUIDITY_FOR_SWAP)
        ;; make sure we can afford to do this swap (have enough output tokens to give back to user)
        (asserts! (< output-amount-sub-fees output-balance) ERR_INSUFFICIENT_LIQUIDITY_FOR_SWAP)

        ;; transfer input token from user to pool
        (try! (contract-call? input-token transfer input-amount sender THIS_CONTRACT none))
        ;; transfer output token from pool to user
        (try! (as-contract (contract-call? output-token transfer output-amount-sub-fees THIS_CONTRACT sender none)))

        ;; update pool balances (x and y)
        (map-set pools pool-id (merge pool-data {
            balance-0: balance-0-post-swap,
            balance-1: balance-1-post-swap
        }))

        (print { action: "swap", pool-id: pool-id, input-amount: input-amount })
        (ok true)
    )
)


;; Compute the hash of (token0 + token1 + fee) to use as a pool ID
(define-read-only (get-pool-id (pool-info {token-0: <ft-trait>, token-1: <ft-trait>, fee: uint})) 
    (let 
        (
            (buff (unwrap-panic (to-consensus-buff? pool-info)))
            (pool-id (hash160 buff))
        )

        pool-id
    )
)

;; get-pool-data
;; Given a pool ID, returns the current state of the pool from the mapping
(define-read-only (get-pool-data (pool-id (buff 20))) 
    (let 
        (
            (pool-data (map-get? pools pool-id))
        )

        (ok pool-data)
    )
)

;; get-position-liquidity
;; Given a Pool ID and a user address, returns how much liquidity the user has in the pool
(define-read-only (get-position-liquidity (pool-id (buff 20)) (owner principal))
    (let
        (
            ;; look up the position in the `positions` map
            (position (map-get? positions { pool-id: pool-id, owner: owner }))
            ;; if position exists, return the liquidity otherwise return 0
            (existing-owner-liquidity (if (is-some position) (unwrap-panic position) {liquidity: u0}))
        )

        (ok (get liquidity existing-owner-liquidity))
    )
)

;; get-pool-info
;; Given a Pool ID, returns detailed pool information including balances, liquidity, fee, and price ratio
(define-read-only (get-pool-info (pool-id (buff 20)))
    (let
        (
            (pool-data (unwrap! (map-get? pools pool-id) (err u0)))
            (balance-0 (get balance-0 pool-data))
            (balance-1 (get balance-1 pool-data))
            (liquidity (get liquidity pool-data))
            (fee (get fee pool-data))
            ;; price as balance-0 / balance-1 with 6 decimal precision
            (price (if (> balance-1 u0) (/ (* balance-0 u1000000) balance-1) u0))
        )
        (ok {
            balance-0: balance-0,
            balance-1: balance-1,
            liquidity: liquidity,
            fee: fee,
            price: price
        })
    )
)


;; Ensure that the token-0 principal is "less than" the token-1 principal
(define-private (correct-token-ordering (token-0 principal) (token-1 principal)) 
    (let
        (
            (token-0-buff (unwrap-panic (to-consensus-buff? token-0)))
            (token-1-buff (unwrap-panic (to-consensus-buff? token-1)))
        )

        (asserts! (< token-0-buff token-1-buff) ERR_INCORRECT_TOKEN_ORDERING)
        (ok true)
    )
)


;; get-amounts
;; Given the desired amount of token-0 and token-1, the minimum amounts of token-0 and token-1, and current reserves of token-0 and token-1,
;; returns the amounts of token-0 and token-1 that should be provided to the pool to meet all constraints
(define-private (get-amounts (amount-0-desired uint) (amount-1-desired uint) (amount-0-min uint) (amount-1-min uint) (balance-0 uint) (balance-1 uint)) 
    (let
        (
            ;; calculate ideal amount of token-1 that should be provided based on the current ratio of reserves if `amount-0-desired` can be fully used
            (amount-1-given-0 (/ (* amount-0-desired balance-1) balance-0))
            ;; calculate ideal amount of token-0 that should be provided based on the current ratio of reserves if `amount-1-desired` can be fully used
            (amount-0-given-1 (/ (* amount-1-desired balance-0) balance-1))
        )

        (if 
            ;; if ideal amount-1 is less than the desired amount-1
            (<= amount-1-given-0 amount-1-desired)
            (begin 
                ;; make sure that ideal amount-1 is >= minimum amount-1 otherwise throw an error
                (asserts! (>= amount-1-given-0 amount-1-min) ERR_INSUFFICIENT_1_AMOUNT)
                ;; we can add amount-0-desired and ideal amount-1 to the pool successfully
                (ok { amount-0: amount-0-desired, amount-1: amount-1-given-0 })
            )
            ;; else if ideal amount-1 is greater than the desired amount-1, we can only add up to `amount-1-desired` to the pool
            (begin 
                ;; make sure that ideal amount-0 is <= desired amount-0 otherwise throw an error
                (asserts! (<= amount-0-given-1 amount-0-desired) ERR_INSUFFICIENT_0_AMOUNT)
                ;; make sure that ideal amount-0 is >= minimum amount-0 otherwise throw an error
                (asserts! (>= amount-0-given-1 amount-0-min) ERR_INSUFFICIENT_0_AMOUNT)
                ;; we can add ideal amount-0 and amount-1-desired to the pool successfully
                (ok { amount-0: amount-0-given-1, amount-1: amount-1-desired })
            )
        )
    )
)

(define-private (min (a uint) (b uint)) 
    (if (< a b) a b)
)

Functions (11)

FunctionAccessArgs
create-poolpublictoken-0: <ft-trait>, token-1: <ft-trait>, fee: uint
add-liquiditypublictoken-0: <ft-trait>, token-1: <ft-trait>, fee: uint, amount-0-desired: uint, amount-1-desired: uint, amount-0-min: uint, amount-1-min: uint
remove-liquiditypublictoken-0: <ft-trait>, token-1: <ft-trait>, fee: uint, liquidity: uint
swappublictoken-0: <ft-trait>, token-1: <ft-trait>, fee: uint, input-amount: uint, zero-for-one: bool
get-pool-idread-onlypool-info: {token-0: <ft-trait>, token-1: <ft-trait>, fee: uint}
get-pool-dataread-onlypool-id: (buff 20
get-position-liquidityread-onlypool-id: (buff 20
get-pool-inforead-onlypool-id: (buff 20
correct-token-orderingprivatetoken-0: principal, token-1: principal
get-amountsprivateamount-0-desired: uint, amount-1-desired: uint, amount-0-min: uint, amount-1-min: uint, balance-0: uint, balance-1: uint
minprivatea: uint, b: uint