[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/useLiquidOps/controller/main/controller.lua [Back]  [Original]

local coroutine = require "coroutine"
local bint = require ".bint"(1024)
local utils = require ".utils"
local json = require "json"

local liquidations = {}
local assertions = {}
local scheduler = {}
local oracle = {}
local tokens = {}
local queue = {}

-- oToken module ID
Module = Module or "C6CQfrL29jZ-LYXV2lKn09d3pBIM6adDFwWqh2ICikM"

-- oracle id and tolerance
Oracle = Oracle or "4fVi8P-xSRWxZ0EE0EpltDe8WJJvcD9QyFXMqfk-1UQ"
MaxOracleDelay = MaxOracleDelay or 1200000

-- admin addresses
Owners = Owners or {}

-- liquidops logo tx id
ProtocolLogo = ProtocolLogo or ""

-- holds all the processes that are part of the protocol
-- a member consists of the following fields:
-- - id: string (this is the address of the collateral supported by LiquidOps)
-- - ticker: string (the ticker of the collateral)
-- - oToken: string (the address of the oToken process for the collateral)
-- - denomination: integer (the denomination of the collateral)
---@type Friend[]
Tokens = Tokens or {}

-- queue for operations that change the user's position
---@type { address: string, origin: string }[]
Queue = Queue or {}

-- current timestamp
Timestamp = Timestamp or 0

-- cached auctions (position wallet address, timestamp when discovered)
---@type table
Auctions = Auctions or {}

-- maximum and minimum discount that can be applied to a loan in percentages
MaxDiscount = MaxDiscount or 5
MinDiscount = MinDiscount or 1

-- the period till the auction reaches the minimum discount (market price)
DiscountInterval = DiscountInterval or 1000 * 60 * 60 -- 1 hour

PrecisionFactor = 1000000

-- minimum liquidation percentage (a liquidator is required to liquidate at least this percentage of the total loan)
MinLiquidationThreshold = MinLiquidationThreshold or 20

---@alias TokenData { ticker: string, denomination: number }
---@alias PriceParam { ticker: string, quantity: Bint?, denomination: number }
---@alias CollateralBorrow { token: string, ticker: string, quantity: string }
---@alias QualifyingPosition { target: string, depts: CollateralBorrow[], collaterals: CollateralBorrow[], discount: string }

Handlers.add(
  "setup-patching",
  function () return "continue" end,
  function (msg)
    ao.send({
      device = "patch@1.0",
      ["token-info"] = { name = "LiquidOps Controller" },
      oracle = Oracle,
      ["discount-config"] = {
        min = MinDiscount,
        max = MaxDiscount,
        interval = DiscountInterval
      },
      tokens = Tokens,
      queue = Queue,
      auctions = Auctions
    })
  end
)

Handlers.add(
  "sync-timestamp",
  function () return "continue" end,
  function (msg) Timestamp = msg.Timestamp end
)

Handlers.add(
  "info",
  { Action = "Info" },
  function (msg)
    msg.reply({
      Name = "LiquidOps Controller",
      Module = Module,
      Oracle = Oracle,
      ["Max-Discount"] = tostring(MaxDiscount),
      ["Min-Discount"] = tostring(MinDiscount),
      ["Discount-Interval"] = tostring(DiscountInterval),
      Data = json.encode(Tokens)
    })
  end
)

Handlers.add(
  "sync-auctions",
  Handlers.utils.hasMatchingTagOf("Action", { "Cron", "Get-Liquidations" }),
  function (msg)
    -- fetch prices first, so the processing of the positions won't be delayed
    local rawPrices = oracle.sync()

    -- generate position messages
    ---@type MessageParam[]
    local positionMsgs = {}

    for _, token in ipairs(Tokens) do
      table.insert(positionMsgs, { Target = token.oToken, Action = "Positions" })
    end

    -- get all user positions
    ---@type Message[]
    local rawPositions = scheduler.schedule(table.unpack(positionMsgs))

    -- protocol positions in USD
    ---@type table
    local allPositions = {}
    local zero = bint.zero()

    -- add positions
    for _, market in ipairs(rawPositions) do
      ---@type boolean, table
      local parsed, marketPositions = pcall(json.decode, market.Data)
      assert(parsed, "Could not parse market data for " .. market.From)

      local ticker = market.Tags["Collateral-Ticker"]
      local denomination = tonumber(market.Tags["Collateral-Denomination"]) or 0
      local collateral = utils.find(
        function (t) return t.oToken == market.From end,
        Tokens
      )

      -- add each position in the market by their usd value
      for address, position in pairs(marketPositions) do
        local posLiquidationLimit = bint(position["Liquidation-Limit"] or 0)
        local posBorrowBalance = bint(position["Borrow-Balance"] or 0)

        local hasCollateral = bint.ult(zero, posLiquidationLimit)
        local hasLoan = bint.ult(zero, posBorrowBalance)

        if hasCollateral or hasLoan then
          allPositions[address] = allPositions[address] or {
            liquidationLimit = zero,
            borrowBalance = zero,
            debts = {},
            collaterals = {}
          }

          -- add liquidation limit
          if hasCollateral and collateral ~= nil then
            allPositions[address].liquidationLimit = allPositions[address].liquidationLimit + oracle.getValue(
              rawPrices,
              posLiquidationLimit,
              ticker,
              denomination
            )
            table.insert(allPositions[address].collaterals, {
              token = collateral.id,
              ticker = ticker,
              quantity = position.Collateralization
            })
          end

          -- add borrow balance
          if hasLoan and collateral ~= nil  then
            allPositions[address].borrowBalance = allPositions[address].borrowBalance + oracle.getValue(
              rawPrices,
              posBorrowBalance,
              ticker,
              denomination
            )
            table.insert(allPositions[address].debts, {
              token = collateral.id,
              ticker = ticker,
              quantity = position["Borrow-Balance"]
            })
          end
        end
      end
    end

    ---@type QualifyingPosition[]
    local qualifyingPositions = {}

    -- now find the positions that can be auctioned
    -- and update existing auctions
    for address, position in pairs(allPositions) do
      -- check if the position can be liquidated
      if bint.ult(position.liquidationLimit, position.borrowBalance) then
        -- add auction
        liquidations.addAuction(address, msg.Timestamp)

        -- calculate discount
        local discount = tokens.getDiscount(address)

        if msg.Tags.Action == "Get-Liquidations" then
          table.insert(qualifyingPositions, {
            target = address,
            debts = position.debts,
            collaterals = position.collaterals,
            discount = discount
          })
        end
      else
        -- remove auction, it is no longer necessary
        liquidations.removeAuction(address)
      end
    end

    if msg.Tags.Action == "Get-Liquidations" then
      msg.reply({
        Data = json.encode({
          liquidations = qualifyingPositions,
          tokens = Tokens,
          maxDiscount = MaxDiscount,
          minDiscount = MinDiscount,
          discountInterval = DiscountInterval,
          prices = rawPrices,
          precisionFactor = PrecisionFactor
        })
      })
    end
  end
)

-- Verify if the caller of an admin function is
-- authorized to run this action
---@param action string Accepted action
---@return PatternFunction
function assertions.isAdminAction(action)
  return function (msg)
    if msg.From ~= ao.env.Process.Id and not utils.includes(msg.From, Owners) then
      return false
    end

    return msg.Tags.Action == action
  end
end

Handlers.add(
  "list",
  assertions.isAdminAction("List"),
  function (msg)
    -- token to be listed
    local token = msg.Tags.Token

    assert(
      assertions.isAddress(token),
      "Invalid token address"
    )
    assert(
      utils.find(function (t) return t.id == token end, Tokens) == nil,
      "Token already listed"
    )

    -- check configuration
    local liquidationThreshold = tonumber(msg.Tags["Liquidation-Threshold"])
    local collateralFactor = tonumber(msg.Tags["Collateral-Factor"])
    local reserveFactor = tonumber(msg.Tags["Reserve-Factor"])
    local baseRate = tonumber(msg.Tags["Base-Rate"])
    local initRate = tonumber(msg.Tags["Init-Rate"])
    local jumpRate = tonumber(msg.Tags["Jump-Rate"])
    local cooldownPeriod = tonumber(msg.Tags["Cooldown-Period"])
    local kinkParam = tonumber(msg.Tags["Kink-Param"])

    assert(
      collateralFactor ~= nil and type(collateralFactor) == "number",
      "Invalid collateral factor"
    )
    assert(
      collateralFactor // 1 == collateralFactor and collateralFactor >= 0 and collateralFactor = 0 and liquidationThreshold  collateralFactor or liquidationThreshold == 0 and collateralFactor == 0,
      "Liquidation threshold must be greater than the collateral factor"
    )
    assert(
      reserveFactor ~= nil and type(reserveFactor) == "number",
      "Invalid reserve factor"
    )
    assert(
      reserveFactor // 1 == reserveFactor and reserveFactor >= 0 and reserveFactor = 0 and kinkParam 

Web Proxy Viewer  |  New URL  |  Original Page