AMM 蓝图

AMM 蓝图为开发者提供了基于 AMM 的模块化模板,允许开发者实现自己的做市算法,或使用内置的两种算法:UniswapV2 曲线 和 恒定价格算法。以下是代码逻辑和功能的详细解析。

核心功能概述

1. 池管理

  • 支持添加新池、和设置池的流动性。

  • 每个池子可以基于不同的做市算法(如 UniswapV2、BigOrder)。

  • 每个池子的状态都被抽象到 FFP.Pools 内进行单独维护。

2. 订单操作

  • 支持用户通过池子完成交易(如兑换交易)

  • 验证订单输入与输出的有效性。

  • 创建满足池子价格的 Note 票据为订单服务。

3. 结算操作

  • 执行 FFP 结算中心结算请求完成 Note 的最终结算。

  • 维护订单的结算状态(如完成结算、过期、拒绝等)。

  • 自动处理已过期或完成的票据。

4. 做市策略灵活性

  • 使用 AlgoToPool 映射表为池子选择不同的做市算法。

  • 开发者可以基于此框架实现新的做市算法。

代码详解

1. 池管理

创建池

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
)

代码逻辑:

  • 检查是否已有相同的池子存在。

  • 指定做市算法创建池子。

  • 将池子加入到 FFP.Pools。

池流动性设置

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
)

代码逻辑:

  • 查找指定池子,并调用池子对象的 updateLiquidity 方法。

  • 更新完成后通知结果。

2. 订单操作

创建订单

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
)

代码逻辑:

  • 校验输入参数和当前池子状态,池子的 Executing 相当于全局锁,确保池子按照先后顺序结算 Note。

  • 调用 pool.getAmountOut 计算交易的输出。

  • 调用 FFP 协议创建票据,并存储到 FFP.Notes。

票据结算执行

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
)

FFP 结算中心会主动调用该接口进行票据结算操作。

代码逻辑:

  • 验证票据和池子状态。

  • 调用 validateAmountOut 验证当前结算的票据是否满足 amm 做市价格。

  • 转移票据上指定的资产到 FFP 结算中心进行结算操作。

结算完成通知

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
)

该方法用于监听 FFP 结算 Note 转移过来的资产或者资产的退款(结算失败)。

代码逻辑:

  • 判断 Note 是否存在。

  • 判断 Note 相关的 pool 是否存在

  • 如果是结算成功,则需要修改 pool 中的做市流动性

  • 更新相关变量。

3. 查询接口

查询 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
)

该方法返回创建的所有 pool 信息。

查询 Note

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
)

该方法根据 MakeTx 查询对应的 Note 并返回。

查询价格

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
)

该方法用于返回实时兑换价格信息。

完整代码

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

开发之前,把以上代码按照路径复制到开发环境即可。

总结

AMM 蓝图通过灵活的架构设计,允许开发者轻松实现和扩展各种 AMM 功能,同时保证交易和流动性管理的高效性。

Last updated

Was this helpful?