AMM Blueprint

The AMM Blueprint provides developers with a modular template for building AMM-based applications. It enables developers to implement custom market-making algorithms or use two built-in algorithms: the UniswapV2 Curve and the Constant Price Algorithm. Below is a detailed analysis of its code logic and features.

Core Features

1. Pool Management

  • Supports adding new pools and setting liquidity for them.

  • Each pool can use different market-making algorithms (e.g., UniswapV2, BigOrder).

  • The state of each pool is maintained independently within FFP.Pools.

2. Order Operations

  • Allows users to execute trades via pools (e.g., swap transactions).

  • Validates the input and output of orders.

  • Creates Notes (tickets) that match pool prices to facilitate orders.

3. Settlement Operations

  • Executes settlement requests from the FFP settlement center to finalize Notes.

  • Maintains the settlement status of orders (e.g., completed, expired, rejected).

  • Automatically handles expired or completed Notes.

4. Flexible Market-Making Strategies

  • Uses the AlgoToPool mapping table to assign different market-making algorithms to pools.

  • Developers can implement new market-making algorithms within this framework.

Detailed Code Analysis

1. Pool Management

Creating a Pool

code
Handlers.add('ffp.addPool', 'FFP.AddPool', 
    function(msg)
        assert(msg.From == Owner or msg.From == ao.id, 'Only owner can add pool')
        assert(type(msg.X) == 'string', 'X is required')
        assert(type(msg.Y) == 'string', 'Y is required')
        assert(msg.X < msg.Y, 'X must be less than Y')
        assert(type(msg.Algo) == 'string', 'Algo is required')
        assert(type(msg.Data) == 'string', 'Params is required')

        local poolId = getPoolId(msg.X, msg.Y)
        if FFP.Pools[poolId] then
            msg.reply({Error = 'Pool already exists'})
            return
        end

        local P = FFP.AlgoToPool[msg.Algo]
        if not P then
            msg.reply({Error = 'Unsupported amm algo'})
            return
        end
        
        local pool = P.new(msg.X, msg.Y, json.decode(msg.Data))
        if not pool then
            msg.reply({Error = 'Invalid pool params'})
            return
        end

        FFP.Pools[poolId] = pool
        msg.reply({Action = 'PoolAdded-Notice', Pool = poolId})
        Send({Target = FFP.Publisher, Action = 'PoolAdded-Notice', Creator = ao.id, Data = json.encode(pool)})
    end
)

Logic:

  • Check if a pool with the same configuration already exists.

  • Specify the market-making algorithm to create the pool.

  • Add the pool to FFP.Pools.

Setting Pool Liquidity

code
Handlers.add('ffp.updateLiquidity', 'FFP.UpdateLiquidity', 
    function(msg)
        assert(msg.From == Owner or msg.From == ao.id, 'Only owner can update liquidity')
        assert(type(msg.Y) == 'string', 'Y is required')
        assert(msg.X < msg.Y, 'X must be less than Y')
        assert(type(msg.Data) == 'string', 'Params is required')
        
        local pool = getPool(msg.X, msg.Y)
        if not pool then
            msg.reply({Error = 'Pool not found'})
            return
        end
        
        local ok = pool:updateLiquidity(json.decode(msg.Data))
        if not ok then
            msg.reply({Error = 'Invalid updateLiquidity params'})
            return
        end

        msg.reply({Action = 'LiquidityUpdated-Notice', Data = json.encode(pool)})
    end
)

Logic:

  • Locate the specified pool and call its updateLiquidity method.

  • Notify the results after updating.

2. Order Operations

Creating Orders

code
Handlers.add('ffp.makeOrder', 'FFP.MakeOrder',
  function(msg)
    if FFP.Executing then
        msg.reply({Error = 'err_executing'})
        return
    end
    
    local ok, err = validateOrderMsg(msg)
    if not ok then
        msg.reply({Error = err})
        return
    end

    local pool = getPool(msg.TokenIn, msg.TokenOut)
    local tokenOut, amountOut = pool:getAmountOut(msg.TokenIn, msg.AmountIn)
    if not amountOut or amountOut == '0' then
        msg.reply({Error = 'err_no_amount_out'})
        return
    end

    if msg.AmountOut then
        local ok, err = validateAmountOut(pool, msg.TokenIn, msg.AmountIn, msg.TokenOut, msg.AmountOut)
        if not ok then
            msg.reply({Error = err})
            return
        end
        amountOut = msg.AmountOut
    end

    -- 90 seconds
    local expireDate = msg.Timestamp + FFP.TimeoutPeriod

    local res = Send({
        Target = FFP.Settle,
        Action = 'CreateNote',
        AssetID = msg.TokenOut,
        Amount = tostring(amountOut),
        HolderAssetID = msg.TokenIn,
        HolderAmount = msg.AmountIn,
        IssueDate = tostring(msg.Timestamp),
        ExpireDate = tostring(expireDate),
        Version = FFP.SettleVersion
    }).receive()
    local noteID = res.NoteID
    local note = json.decode(res.Data)
    note.MakeTx = msg.Id
    FFP.Notes[noteID] = note
    FFP.MakeTxToNoteID[msg.Id] = noteID
    -- remove expired notes in Notes
    for noteID, note in pairs(FFP.Notes) do
        if note.Status == 'Open' and note.ExpireDate and note.ExpireDate < msg.Timestamp then
            FFP.Notes[noteID] = nil
            FFP.MakeTxToNoteID[note.MakeTx] = nil
        end
    end

    msg.reply({
        Action = 'OrderMade-Notice',
        NoteID = noteID,
        Data = json.encode(note)
    })
  end
)

Logic:

  • Validate input parameters and the current pool state. The pool’s Executing status acts as a global lock to ensure Notes are settled sequentially.

  • Call pool.getAmountOut to calculate the trade’s output.

  • Use the FFP protocol to create a Note and store it in FFP.Notes.

Order Settlement Execution

code
Handlers.add('ffp.execute', 'Execute',
  function(msg)
    assert(msg.From == FFP.Settle, 'Only settle can start exectue')
    assert(type(msg.NoteID) == 'string', 'NoteID is required')
    assert(type(msg.SettleID) == 'string', 'SettleID is required')

    if FFP.Executing then
        msg.reply({Action= 'Reject', Error = 'err_executing', SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end

    local note = FFP.Notes[msg.NoteID]
    if not note then
        msg.reply({Action= 'Reject', Error = 'err_not_found', SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end
    if note.Status ~= 'Open' then
        msg.reply({Action= 'Reject', Error = 'err_not_open', SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end
    if note.Issuer ~= ao.id then
        msg.reply({Action = 'Reject', Error = 'err_invalid_issuer', SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end
    if note.ExpireDate and note.ExpireDate < msg.Timestamp then
        FFP.Notes[note.NoteID] = nil
        FFP.MakeTxToNoteID[note.MakeTx] = nil
        msg.reply({Action= 'Reject', Error = 'err_expired', SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end

    local pool = getPool(note.HolderAssetID, note.AssetID)
    local ok, err = validateAmountOut(pool, note.HolderAssetID, note.HolderAmount, note.AssetID, note.Amount)
    if not ok then
        msg.reply({Action= 'Reject', Error = err, SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end

    msg.reply({Action = 'ExecuteStarted', SettleID = msg.SettleID, NoteID = msg.NoteID})

    FFP.Executing = true
    note.Status = 'Executing'

    Send({Target = note.AssetID, Action = 'Transfer', Quantity = note.Amount, Recipient = FFP.Settle, 
      ['X-FFP-SettleID'] = msg.SettleID, 
      ['X-FFP-NoteID'] = msg.NoteID,
      ['X-FFP-For'] = 'Execute'
    })
  end
)

Triggered by the FFP settlement center for Note settlement operations.

Logic:

  • Validate the Note and pool state.

  • Use validateAmountOut to verify if the Note matches the AMM market-making price.

  • Transfer the assets specified in the Note to the FFP settlement center for settlement.

Settlement Completion Notification

code
Handlers.add('ffp.done',
    function(msg) return (msg.Action == 'Credit-Notice') and (msg['X-FFP-For'] == 'Settled' or msg['X-FFP-For'] == 'Refund') end,
    function(msg)
        assert(msg.Sender == FFP.Settle, 'Only settle can send settled or refund notice')
        
        local noteID = msg['X-FFP-NoteID']
        local note = FFP.Notes[noteID]
        if not note then
            print('no note found when settled: ' .. noteID)
            return 
        end

        local pool = getPool(note.HolderAssetID, note.AssetID)
        if msg['X-FFP-For'] == 'Settled' then
            pool:updateAfterSwap(note.HolderAssetID, note.HolderAmount, note.AssetID, note.Amount)
        end
        FFP.Notes[noteID].Status = msg['X-FFP-For']
        FFP.Executing = false

        FFP.Notes[noteID] = json.decode(Send({Target = FFP.Settle, Action = 'GetNote', NoteID = noteID}).receive().Data)
        
    end
)

Listens for asset transfers or refunds (in case of settlement failure) from the FFP settlement center.

Logic:

  • Check if the Note exists.

  • Ensure the related pool exists.

  • For successful settlements, update the pool’s market-making liquidity.

  • Update relevant variables.

3. Query Interfaces

Query Pools: Returns information about all created pools.

code
Handlers.add('ffp.pools', 'FFP.GetPools', 
    function(msg)
        local pools = {}
        for poolId, pool in pairs(FFP.Pools) do
            pools[poolId] = pool:info()
        end
        msg.reply({Data = json.encode(pools)})
    end
)

Query Notes: Retrieves a specific Note based on its MakeTx.

code
Handlers.add('ffp.getNote', 'FFP.GetNote', 
    function(msg)
        assert(type(msg.MakeTx) == 'string', 'MakeTx is required')
        local note = FFP.Notes[FFP.MakeTxToNoteID[msg.MakeTx]] or ''
        msg.reply({NoteID=note.NoteID, Data = json.encode(note)})
    end
)

Query Prices: Provides real-time exchange price information.

code
Handlers.add('ffp.getAmountOut', 'FFP.GetAmountOut',
  function(msg)
    if FFP.Executing then
        msg.reply({Error = 'err_executing'})
        return
    end
    
    local ok, err = validateOrderMsg(msg)
    if not ok then
        msg.reply({Error = err})
        return
    end

    local pool = getPool(msg.TokenIn, msg.TokenOut)
    local tokenOut, amountOut = pool:getAmountOut(msg.TokenIn, msg.AmountIn)
    if not amountOut or amountOut == '0' then
        msg.reply({Error = 'err_no_amount_out'})
        return
    end
    msg.reply({
        TokenIn = msg.TokenIn,
        AmountIn = msg.AmountIn,
        TokenOut = tokenOut,
        AmountOut = tostring(amountOut)
    })
  end
)

Complete Code

amm.lua
local json = require('json')
local bint = require('.bint')(512)
local utils = require('.utils')

local poolUniswapV2 = require('.algo.uniswapv2')
local poolBigOrder = require('.algo.bigorder')

FFP = FFP or {}
-- config
FFP.Settle = FFP.Settle or 'rKpOUxssKxgfXQOpaCq22npHno6oRw66L3kZeoo_Ndk'
FFP.Publisher = FFP.Publisher or '8VzqSX_0rdAr99P3QIYd0bRG-XySTQUNFlfJf20zNEo'
FFP.SettleVersion = FFP.SettleVersion or '0.31'
FFP.MaxNotesToSettle = FFP.MaxNotesToSettle or 2
FFP.TimeoutPeriod = FFP.TimeoutPeriod or 90000

-- database or state
FFP.Pools = FFP.Pools or {}
FFP.Notes = FFP.Notes or {}
FFP.MakeTxToNoteID = FFP.MakeTxToNoteID or {}
FFP.Executing = FFP.Executing or false

-- amm pool functions
FFP.AlgoToPool = FFP.AlgoToPool or {
    UniswapV2 = poolUniswapV2,
    BigOrder = poolBigOrder
}

local function getPoolId(X, Y)
    if X > Y then
        return Y .. ':' .. X
    end
    return X .. ':' .. Y
end

local function getPool(X, Y)
    return FFP.Pools[getPoolId(X, Y)]
end

local function validateAmount(amount)
    if not amount then
        return false, 'err_no_amount'
    end
    local ok, qty = pcall(bint, amount)
    if not ok then
        return false, 'err_invalid_amount'
    end
    if not bint.__lt(0, qty) then
        return false, 'err_negative_amount'
    end
    return true, nil
end

-- tokenIn, tokenOut, amountIn, amountOut(optional)
local function validateOrderMsg(msg)
    local Pool = getPool(msg.TokenIn, msg.TokenOut)
    if not Pool then
        return false, 'err_pool_not_found'
    end
    
    local ok, err = validateAmount(msg.AmountIn)
    if not ok then
        return false, 'err_invalid_amount_in'
    end
    
    if msg.AmountOut then
        local ok, err = validateAmount(msg.AmountOut)
        if not ok then
            return false, 'err_invalid_amount_out'
        end
    end

    return true, nil
end

local function validateAmountOut(pool, tokenIn, amountIn, tokenOut, expectedAmountOut)
    local tokenOut_, amountOut = pool:getAmountOut(tokenIn, amountIn)
    if not amountOut or amountOut == '0' then
        return false, 'err_no_amount_out'
    end
    if tokenOut_ ~= tokenOut then
        return false, 'err_invalid_token_out'
    end
    if expectedAmountOut then
        if utils.lt(amountOut, expectedAmountOut) then
            return false, 'err_amount_out_too_small'
        end
    end 
    return true, nil
end

Handlers.add('ffp.withdraw', 'FFP.Withdraw',
  function(msg)
    assert(msg.From == Owner or msg.From == ao.id, 'Only owner can withdraw')
    assert(type(msg.Token) == 'string', 'Token is required')
    assert(type(msg.Amount) == 'string', 'Amount is required')
    assert(bint.__lt(0, bint(msg.Amount)), 'Amount must be greater than 0')
    
    Send({ 
      Target = msg.Token, 
      Action = 'Transfer', 
      Quantity = msg.Amount, 
      Recipient = Owner
    })
  end
)

Handlers.add('ffp.pools', 'FFP.GetPools', 
    function(msg)
        local pools = {}
        for poolId, pool in pairs(FFP.Pools) do
            pools[poolId] = pool:info()
        end
        msg.reply({Data = json.encode(pools)})
    end
)

Handlers.add('ffp.addPool', 'FFP.AddPool', 
    function(msg)
        assert(msg.From == Owner or msg.From == ao.id, 'Only owner can add pool')
        assert(type(msg.X) == 'string', 'X is required')
        assert(type(msg.Y) == 'string', 'Y is required')
        assert(msg.X < msg.Y, 'X must be less than Y')
        assert(type(msg.Algo) == 'string', 'Algo is required')
        assert(type(msg.Data) == 'string', 'Params is required')

        local poolId = getPoolId(msg.X, msg.Y)
        if FFP.Pools[poolId] then
            msg.reply({Error = 'Pool already exists'})
            return
        end

        local P = FFP.AlgoToPool[msg.Algo]
        if not P then
            msg.reply({Error = 'Unsupported amm algo'})
            return
        end
        
        local pool = P.new(msg.X, msg.Y, json.decode(msg.Data))
        if not pool then
            msg.reply({Error = 'Invalid pool params'})
            return
        end

        FFP.Pools[poolId] = pool
        msg.reply({Action = 'PoolAdded-Notice', Pool = poolId})
        Send({Target = FFP.Publisher, Action = 'PoolAdded-Notice', Creator = ao.id, Data = json.encode(pool)})
    end
)

Handlers.add('ffp.updateLiquidity', 'FFP.UpdateLiquidity', 
    function(msg)
        assert(msg.From == Owner or msg.From == ao.id, 'Only owner can update liquidity')
        assert(type(msg.Y) == 'string', 'Y is required')
        assert(msg.X < msg.Y, 'X must be less than Y')
        assert(type(msg.Data) == 'string', 'Params is required')
        
        local pool = getPool(msg.X, msg.Y)
        if not pool then
            msg.reply({Error = 'Pool not found'})
            return
        end
        
        local ok = pool:updateLiquidity(json.decode(msg.Data))
        if not ok then
            msg.reply({Error = 'Invalid updateLiquidity params'})
            return
        end

        msg.reply({Action = 'LiquidityUpdated-Notice', Data = json.encode(pool)})
    end
)

Handlers.add('ffp.getAmountOut', 'FFP.GetAmountOut',
  function(msg)
    if FFP.Executing then
        msg.reply({Error = 'err_executing'})
        return
    end
    
    local ok, err = validateOrderMsg(msg)
    if not ok then
        msg.reply({Error = err})
        return
    end

    local pool = getPool(msg.TokenIn, msg.TokenOut)
    local tokenOut, amountOut = pool:getAmountOut(msg.TokenIn, msg.AmountIn)
    if not amountOut or amountOut == '0' then
        msg.reply({Error = 'err_no_amount_out'})
        return
    end
    msg.reply({
        TokenIn = msg.TokenIn,
        AmountIn = msg.AmountIn,
        TokenOut = tokenOut,
        AmountOut = tostring(amountOut)
    })
  end
)

Handlers.add('ffp.makeOrder', 'FFP.MakeOrder',
  function(msg)
    if FFP.Executing then
        msg.reply({Error = 'err_executing'})
        return
    end
    
    local ok, err = validateOrderMsg(msg)
    if not ok then
        msg.reply({Error = err})
        return
    end

    local pool = getPool(msg.TokenIn, msg.TokenOut)
    local tokenOut, amountOut = pool:getAmountOut(msg.TokenIn, msg.AmountIn)
    if not amountOut or amountOut == '0' then
        msg.reply({Error = 'err_no_amount_out'})
        return
    end

    if msg.AmountOut then
        local ok, err = validateAmountOut(pool, msg.TokenIn, msg.AmountIn, msg.TokenOut, msg.AmountOut)
        if not ok then
            msg.reply({Error = err})
            return
        end
        amountOut = msg.AmountOut
    end

    -- 90 seconds
    local expireDate = msg.Timestamp + FFP.TimeoutPeriod

    local res = Send({
        Target = FFP.Settle,
        Action = 'CreateNote',
        AssetID = msg.TokenOut,
        Amount = tostring(amountOut),
        HolderAssetID = msg.TokenIn,
        HolderAmount = msg.AmountIn,
        IssueDate = tostring(msg.Timestamp),
        ExpireDate = tostring(expireDate),
        Version = FFP.SettleVersion
    }).receive()
    local noteID = res.NoteID
    local note = json.decode(res.Data)
    note.MakeTx = msg.Id
    FFP.Notes[noteID] = note
    FFP.MakeTxToNoteID[msg.Id] = noteID
    -- remove expired notes in Notes
    for noteID, note in pairs(FFP.Notes) do
        if note.Status == 'Open' and note.ExpireDate and note.ExpireDate < msg.Timestamp then
            FFP.Notes[noteID] = nil
            FFP.MakeTxToNoteID[note.MakeTx] = nil
        end
    end

    msg.reply({
        Action = 'OrderMade-Notice',
        NoteID = noteID,
        Data = json.encode(note)
    })
  end
)

Handlers.add('ffp.getNote', 'FFP.GetNote', 
    function(msg)
        assert(type(msg.MakeTx) == 'string', 'MakeTx is required')
        local note = FFP.Notes[FFP.MakeTxToNoteID[msg.MakeTx]] or ''
        msg.reply({NoteID=note.NoteID, Data = json.encode(note)})
    end
)

Handlers.add('ffp.execute', 'Execute',
  function(msg)
    assert(msg.From == FFP.Settle, 'Only settle can start exectue')
    assert(type(msg.NoteID) == 'string', 'NoteID is required')
    assert(type(msg.SettleID) == 'string', 'SettleID is required')

    if FFP.Executing then
        msg.reply({Action= 'Reject', Error = 'err_executing', SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end

    local note = FFP.Notes[msg.NoteID]
    if not note then
        msg.reply({Action= 'Reject', Error = 'err_not_found', SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end
    if note.Status ~= 'Open' then
        msg.reply({Action= 'Reject', Error = 'err_not_open', SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end
    if note.Issuer ~= ao.id then
        msg.reply({Action = 'Reject', Error = 'err_invalid_issuer', SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end
    if note.ExpireDate and note.ExpireDate < msg.Timestamp then
        FFP.Notes[note.NoteID] = nil
        FFP.MakeTxToNoteID[note.MakeTx] = nil
        msg.reply({Action= 'Reject', Error = 'err_expired', SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end

    local pool = getPool(note.HolderAssetID, note.AssetID)
    local ok, err = validateAmountOut(pool, note.HolderAssetID, note.HolderAmount, note.AssetID, note.Amount)
    if not ok then
        msg.reply({Action= 'Reject', Error = err, SettleID = msg.SettleID, NoteID = msg.NoteID})
        return
    end

    msg.reply({Action = 'ExecuteStarted', SettleID = msg.SettleID, NoteID = msg.NoteID})

    FFP.Executing = true
    note.Status = 'Executing'

    Send({Target = note.AssetID, Action = 'Transfer', Quantity = note.Amount, Recipient = FFP.Settle, 
      ['X-FFP-SettleID'] = msg.SettleID, 
      ['X-FFP-NoteID'] = msg.NoteID,
      ['X-FFP-For'] = 'Execute'
    })
  end
)

Handlers.add('ffp.done',
    function(msg) return (msg.Action == 'Credit-Notice') and (msg['X-FFP-For'] == 'Settled' or msg['X-FFP-For'] == 'Refund') end,
    function(msg)
        assert(msg.Sender == FFP.Settle, 'Only settle can send settled or refund notice')
        
        local noteID = msg['X-FFP-NoteID']
        local note = FFP.Notes[noteID]
        if not note then
            print('no note found when settled: ' .. noteID)
            return 
        end

        local pool = getPool(note.HolderAssetID, note.AssetID)
        if msg['X-FFP-For'] == 'Settled' then
            pool:updateAfterSwap(note.HolderAssetID, note.HolderAmount, note.AssetID, note.Amount)
        end
        FFP.Notes[noteID].Status = msg['X-FFP-For']
        FFP.Executing = false

        FFP.Notes[noteID] = json.decode(Send({Target = FFP.Settle, Action = 'GetNote', NoteID = noteID}).receive().Data)
        
    end
)
utils.lua
local json = require('json')
local bint = require('.bint')(512)

local mod = {
  add = function(a, b)
      return tostring(bint(a) + bint(b))
  end,
  subtract = function(a, b)
      return tostring(bint(a) - bint(b))
  end,
  toBalanceValue = function(a)
      return tostring(bint(a))
  end,
  toNumber = function(a)
      return tonumber(a)
  end,
  lt = function (a, b)
      return bint.__lt(bint(a), bint(b))
  end
}

mod.getNoteIDs = function(data)
  if string.find(data, "null") then
    return nil
  end
  
  local noteIDs, err = json.decode(data)
  if err then
    return nil
  end

  if type(noteIDs) == "string" then
    return {noteIDs}
  end

  if type(noteIDs) == "table" then
    for i = 1, #noteIDs do
      if noteIDs[i] == nil or type(noteIDs[i]) ~= "string" then
        return nil
      end
    end
    return noteIDs
  end

  return nil
end

mod.SettlerIO = function (notes)
  local settlerIn = {}
  local settlerOut = {}

  local bc = {}
  for _, note in ipairs(notes) do
      if not bc[note.AssetID] then bc[note.AssetID] = bint(0) end
      if not bc[note.HolderAssetID] then bc[note.HolderAssetID] = bint(0) end
      bc[note.AssetID] = mod.add(bc[note.AssetID], note.Amount)
      bc[note.HolderAssetID] = mod.subtract(bc[note.HolderAssetID], note.HolderAmount)
  end
  
  for k, v in pairs(bc) do
      if v == '0' then bc[k] = nil end
  end

  for k, v in pairs(bc) do
      if mod.lt(v, '0') then
          settlerOut[k] = v
      else
          settlerIn[k] = v
      end
  end

  return settlerIn, settlerOut
end
    
return mod
./algo/bigorder.lua
local bint = require('.bint')(1024)

local Pool = {}
Pool.__index = Pool 

local utils = {
    add = function (a,b) 
      return tostring(bint(a) + bint(b))
    end,
    subtract = function (a,b)
      return tostring(bint(a) - bint(b))
    end,
    mul = function(a, b)
        return tostring(bint.__mul(bint(a), bint(b)))
    end,
    div = function(a, b)
        return tostring(bint.udiv(bint(a), bint(b)))
    end,
    lt = function (a, b)
        return bint.__lt(bint(a), bint(b))
    end
}

local function validateAmount(amount)
    if not amount then
        return false, 'err_no_amount'
    end
    local ok, qty = pcall(bint, amount)
    if not ok then
        return false, 'err_invalid_amount'
    end
    if not utils.lt(0, qty) then
        return false, 'err_negative_amount'
    end
    return true, nil
end

local function validateInitParams(params)
    if not params then
        return false
    end
    return params.tokenOut and params.tokenIn and validateAmount(params.amountOut) and validateAmount(params.amountIn)
end

function Pool.new(x, y, params)
    if not validateInitParams(params) then
        return nil
    end
    local self = setmetatable({}, Pool)
    self.algo = 'BigOrder'
    self.x = x
    self.y = y
    self.tokenIn = params.tokenIn
    self.tokenOut = params.tokenOut
    self.amountOut = params.amountOut
    self.amountIn = params.amountIn
    self.balanceTokenIn = '0'
    self.balanceTokenOut = self.amountOut
    return self
end

function Pool:balances()
    local balance = {
        [self.tokenOut] = self.balanceTokenOut,
        [self.tokenIn] = self.balanceTokenIn
    }
    return balance
end

function Pool:info()
    local info = {
        x = self.x,
        y = self.y,
        algo = self.algo,
        tokenIn = self.tokenIn,
        tokenOut = self.tokenOut,
        amountOut = self.amountOut,
        amountIn = self.amountIn,
        balances = self:balances()
    }
    return info
end

function Pool:updateLiquidity(params)
    return false
end

function Pool:updateAfterSwap(tokenIn, amountIn, tokenOut, amountOut)
   self.balanceTokenIn = utils.add(self.balanceTokenIn, amountIn)
   self.balanceTokenOut = utils.subtract(self.balanceTokenOut, amountOut)
end

function Pool:getAmountOut(tokenIn, amountIn)
    if tokenIn == self.tokenIn then
        local amountOut = utils.div(utils.mul(amountIn, self.amountOut), self.amountIn)
        if utils.lt(amountOut, self.balanceTokenOut) then
            return self.tokenOut, tostring(amountOut)
        end
    end

    return nil, nil
end

return Pool
./algo/uniswapv2.lua
local bint = require('.bint')(1024)

local Pool = {}
Pool.__index = Pool 

local utils = {
    add = function (a,b) 
      return tostring(bint(a) + bint(b))
    end,
    subtract = function (a,b)
      return tostring(bint(a) - bint(b))
    end,
    mul = function(a, b)
        return tostring(bint.__mul(bint(a), bint(b)))
    end,
    div = function(a, b)
        return tostring(bint.udiv(bint(a), bint(b)))
    end,
    lt = function (a, b)
        return bint.__lt(bint(a), bint(b))
    end
}

local function validateAmount(amount)
    if not amount then
        return false, 'err_no_amount'
    end
    local ok, qty = pcall(bint, amount)
    if not ok then
        return false, 'err_invalid_amount'
    end
    if not utils.lt(0, qty) then
        return false, 'err_negative_amount'
    end
    return true, nil
end

local function getAmountOut(amountIn, reserveIn, reserveOut, fee)
    local amountInWithFee = utils.mul(amountIn, utils.subtract(10000, fee))
    local numerator = utils.mul(amountInWithFee, reserveOut)
    local denominator = utils.add(utils.mul(10000, reserveIn), amountInWithFee)
    return utils.div(numerator, denominator)
end

local function validateInitParams(params)
    if not params then
        return false
    end
    return validateAmount(params.fee) and validateAmount(params.px) and validateAmount(params.py)
end

local function validateUpdateLiquidityParams(params)
    if not params then
        return false
    end
    return validateAmount(params.px) and validateAmount(params.py)
end

function Pool.new(x, y, params)
    if not validateInitParams(params) then
        return nil
    end
    local self = setmetatable({}, Pool)
    self.algo = 'UniswapV2'
    self.x = x
    self.y = y
    self.fee = params.fee
    self.px = params.px
    self.py = params.py
    return self
end

function Pool:balances()
    local balance = {
        [self.x] = self.px,
        [self.y] = self.py
    }
    return balance
end

function Pool:info()
    local info = {
        x = self.x,
        y = self.y,
        fee = self.fee,
        algo = self.algo,
        px = self.px,
        py = self.py,
        balances = self:balances()
    }
    return info
end

function Pool:updateLiquidity(params)
    if not validateUpdateLiquidityParams(params) then
        return false
    end
    self.px = params.px
    self.py = params.py
    return true
end

function Pool:updateAfterSwap(tokenIn, amountIn, tokenOut, amountOut)
    if tokenIn == self.x then
        self.px = utils.add(self.px, amountIn)
        self.py = utils.subtract(self.py, amountOut)
    else
        self.py = utils.add(self.py, amountIn)
        self.px = utils.subtract(self.px, amountOut)
    end
end

function Pool:getAmountOut(tokenIn, amountIn)
    local tokenOut = self.y
    local reserveIn = bint(self.px)
    local reserveOut = bint(self.py)
    if tokenIn == self.y then
        reserveIn, reserveOut = reserveOut, reserveIn
        tokenOut = self.x
    end
    local amountOut = getAmountOut(bint(amountIn), reserveIn, reserveOut, bint(self.fee))
    return tokenOut, tostring(amountOut)
end

return Pool

Before starting development, copy the above code into your development environment as per the specified paths.

Summary

The AMM Blueprint uses a flexible architectural design to enable developers to easily implement and extend various AMM functionalities. It ensures high efficiency in transaction handling and liquidity management, making it a robust solution for building custom AMM applications.

Last updated

Was this helpful?