Smart Contract Development Guide

Intro

SakePerp.fi is a decentralized perpetual contracts platform based on the Ethereum virtual machine. This doc introduces the interface and call method of the SakePerp contracts. Users are able to interact with the contract directly through languages ​​such as Javascript/ Typescript/ Go and develop their own advanced trading strategy.

Contract Addresses and abi

  • Smart Contract Addresses(BSC - Mainnet)

    url: https://bsc-graphnode-api.sakeperp.fi/subgraphs/name/sakeperp/sakeperp-subgraph
    Request this url by post, and set the data {"query":"{contractLists {isExchange name addr exchangeStateAddr insuranceFundAddr highRiskLPTokenAddr quoteAssetName quoteAssetAddr maxLeverage}}"}
    
    The return value is in json format
    {
      "data": {
        "contractLists": [
          {
            "addr": "0x97dc089519d0b341D401a4fBaC26B02a7DCd7f6b",    // Contract Address
            "exchangeStateAddr": "0xB04Af392cF7c2d09A6dC8a812DEDCe02Ef4D2092",    // state contract address of the corresponding trading pool
            "highRiskLPTokenAddr": "0xa1e38c2710b71cad2e2e6afa25eeae26dd3b34bb", // LPToken contract address of the corresponding trading pool
            "insuranceFundAddr": "0x7395DE44d09E2c691F1bd946F660BbFCf5718275",    // Insurance Fund contract address of the corresponding trading pool 
            "isExchange": true,    // Is it a trading pool address?
            "maxLeverage": 15,    // Maximum leverage supported by the trading pool
            "name": "ETH/BUSD",    // Contract name
            "quoteAssetAddr": "0xe9e7cea3dedca5984780bafc599bd69add087d56", // Contract address of the asset used for a quote 
            "quoteAssetName": "BUSD"    // The name of the asset used for a quote
          },
          {
            "addr": "0xCFADd8b52B0737401d8e0aFDc8Aa9194bd40BFc3",
            "exchangeStateAddr": "0x79366156125D61883d09036d9b57d36d897F9895",
            "highRiskLPTokenAddr": "0xeca8f31532b8b51f2185210f22fbe3c33a786b1b",
            "insuranceFundAddr": "0xCAe9E683864E5892DA9d258cae4C4d029BD10620",
            "isExchange": true,
            "maxLeverage": 15,
            "name": "BTC/BUSD",
            "quoteAssetAddr": "0xe9e7cea3dedca5984780bafc599bd69add087d56",
            "quoteAssetName": "BUSD"
          },
          {
            "addr": "0xeCf9e5c123a1E251E4f9E3B9800EC8ab3E5033Ed",    // Contract address
            "exchangeStateAddr": "",
            "highRiskLPTokenAddr": "",
            "insuranceFundAddr": "",
            "isExchange": false,
            "maxLeverage": 0,
            "name": "SakePerp",    // Contract name
            "quoteAssetAddr": "",
            "quoteAssetName": ""
          },
          ......
        ]
      }
    }
  const fetch = require("cross-fetch")
  const url = "https://bsc-graphnode-api.sakeperp.fi/subgraphs/name/sakeperp/sakeperp-subgraph"
  const metadata = await fetch(url, { method: 'POST', body: '{"query": "{contractLists {isExchange name addr exchangeStateAddr insuranceFundAddr highRiskLPTokenAddr quoteAssetName quoteAssetAddr maxLeverage}}"}' })
  const contracts = metadata.data.contractLists
  for (let i = 0; i < contracts.length; i++) {
    if (contracts[i].name == "SakePerp") {
      sakePerpAddr = contracts[i].addr
    } else if (contracts[i].name == "SystemSettings") {
      systemSettingsAddr = contracts[i].addr
    } else if (contracts[i].name == "SakePerpViewer") {
      sakePerpViewerAddr = contracts[i].addr
    } else if (contracts[i].name == "ExchangeReader") {
      exchangeReaderAddr = contracts[i].addr
    } else if  (contracts[i].name == "SakePerpState") {
      sakePerpStateAddr = contracts[i].addr 
    } else if (contracts[i].isExchange == true) {
      exchangeAddr[contracts[i].name] = contracts[i].addr    // Trading pool address
    } else if (contracts[i].name == "QuoteAsset") {
      quoteAssetAddr = contracts[i].addr
    }
  }
  • abi

    Abi for all contracts are available from npm
    npm install @sakeperp/artifact

Contract Interface and Calls

Users need an account on BSC before the smart contract interation. User can initialise an account with a private key or a mnemonic. Please make sure that there's enough Gas and BUSD in the account.

const { Contract, providers, Wallet } = require("ethers")
const bscNodeUrl = "https://bsc-dataseed1.defibit.io/"
const provider = new providers.JsonRpcProvider(bscNodeUrl)
const wallet = new Wallet(privateKey, provider)    // use privatekey
// const wallet = Wallet.fromMnemonic(mnemonic).connect(provider) // use mnemonic

Users can start making contract calls when one has the BSC account and related smart contract information.

const ExchangeAbi = require("@sakeperp/artifact/src/Exchange.json")
const ExchangeReaderAbi = require("@sakeperp/artifact/src/ExchangeReader.json")
const SakePerpAbi = require("@sakeperp/artifact/src/SakePerp.json")
const SakePerpViewerAbi = require("@sakeperp/artifact/src/SakePerpViewer.json")
const SakePerpVaultAbi = require("@sakeperp/artifact/src/SakePerpVault.json")
const Erc20Abi = require("@sakeperp/artifact/src/ERC20.json")

const sakePerp = new Contract(sakePerpAddr, SakePerpAbi, wallet)
const sakePerpViewer = new Contract(sakePerpViewerAddr, SakePerpViewerAbi, wallet)
const sakePerpVault = new Contract(sakePerpVaultAddr, SakePerpVaultAbi, wallet)
const quoteAsset = new Contract(quoteAssetAddr, Erc20Abi, wallet)

Before the contract call, we need to unify different tokens into the same decimal place when we do mathematical calculations to avoid errors.

In order to avoid such errors, the Sake dev team has developed a set of libraries in the contract to perform these kinds of mathematical calculations. Some contract interface entries will require a javascript object to be inputted. There is only one d attribute. All d values require 18 decimal places. For example, if we want a leverage multiple of 1.5, then we need to input 15000000000000000000000.

const { parseUnits } = require("ethers/lib/utils")
const converted = { d: parseUnits("1.5", 18) } // d is 1500000000000000000

Position Operation and Enquiry

SakePerp provides interfaces to open, close, add and remove margins.

  • approve

    Before opening a position, users need to authorise the SakePerp contract to have access to their account funds.

  const tx = await quoteAsset.approve(sakePerpAddr, constants.MaxUint256)
  await tx.wait()
  • openPosition(Open Position)

  openPosition(
    IExchange _exchange,
    Side _side,
    Decimal.decimal calldata _quoteAssetAmount,
    Decimal.decimal calldata _leverage,
    Decimal.decimal calldata _baseAssetAmountLimit
  )

  _exchange:address of the trading pool
  _side:0:Long 1:Short
  _quoteAssetAmount:Margin amount
  _leverage:leverage
  _baseAssetAmountLimit:The minimum position requirement is set to avoid excessive slippage. If the actual position is less than the mininimum position, the trade will fail. 0 min position means that any position size is acceptable.


  const { parseUnits } = require("ethers/lib/utils")
  const DEFAULT_DECIMALS = 18
  const side = 1 // Short
  const quoteAssetAmount = { d: parseUnits("1000", DEFAULT_DECIMALS) }
  const leverage = { d: parseUnits("1.5", DEFAULT_DECIMALS) }
  const minBaseAssetAmount = { d: "0" }
  const options = { gasLimit: 2_500_000 } 
  const tx = await sakePerp.openPosition(
    exchangeAddr,
    side,
    quoteAssetAmount,
    leverage,
    minBaseAssetAmount,
    options,
  )
  await tx.wait()
  • closePosition(Close Position)

  closePosition() will close all the positions of the account in the specified trading pool. If users wants to close part of the positions, he can open a position opposite to the original position through openPosition()

  closePosition(IExchange _exchange, Decimal.decimal calldata _quoteAssetAmountLimit)

  _exchange:Trading pool address
  _quoteAssetAmountLimit:The minimum funds requirement. The transaction will fail if below the value. Setting it to 0 means accepting any amount of funds.

  const { parseUnits } = require("ethers/lib/utils")
  const DEFAULT_DECIMALS = 18
  const minQuoteAssetAmount = { d: "0" }
  const options = { gasLimit: 2_500_000 } 
  const tx = await sakePerp.closePosition(
    exchangeAddr,
    minQuoteAssetAmount,
    options,
  )
  await tx.wait()
  • addMargin(Add Margin)

  addMargin(IExchange _exchange, Decimal.decimal calldata _addedMargin)

  _exchange:Trading pool address
  _addedMargin:The amount of margin to add
  • removeMargin(Remove Margin)

  removeMargin(IExchange _exchange, Decimal.decimal calldata _removedMargin)

  _exchange:Trading pool address
  _removedMargin:The amount of margin to be removed. After removing the margin, the margin ratio should still be greater than the maintenance margin ratio.

SakePerpViewer Contract provides a position enquiry interface.

  • getUnrealizedPnl(Query the user's unrealized profit and loss)

  getUnrealizedPnl(
    IExchange _exchange,
    address _trader,
    ISakePerp.PnlCalcOption _pnlCalcOption
  )

  _exchange:Trading pool address
  _trader: User's address
  _pnlCalcOption:Fixed with 0
  • getPersonalBalanceWithFundingPayment (Query the user's balance in all trading pools)

  getPersonalBalanceWithFundingPayment(IERC20Upgradeable _quoteToken, address _trader)

  _quoteToken: The token address of the quoted asset. Currently it's the contract address of BUSD.
  _trader:Users's address
  • getPersonalPositionWithFundingPayment (Query the user's position information in the specified trading pool)

  getPersonalPositionWithFundingPayment(IExchange _exchange, address _trader)

  _exchange: Trading pool address
  _trader: User's address

  Return Value
  struct Position {
    SignedDecimal.signedDecimal size;
    Decimal.decimal margin;
    Decimal.decimal openNotional;
    SignedDecimal.signedDecimal lastUpdatedCumulativePremiumFraction;
    Decimal.decimal lastUpdatedCumulativeOvernightFeeRate;
    uint256 liquidityHistoryIndex;
    uint256 blockNumber;
  }

  size: Position size
  margin: Margin
  openNotional: Total position value
  • getMarginRatio (Query the user's margin ratio in the specified trading pool)

    When the margin ratio of the non-stablecoin trading pool is less than 3%, and the margin ratio of the stablecoin trading pool is less than 1%, it will be automatically liquidated.
    
    getMarginRatio(IExchange _exchange, address _trader)
    
    _exchange: Trading pool address
    _trader: User's address

MM Liquidity Operation and Enquiry

SakePerpVault provides interfaces for adding, removing, and querying liquidity to MMs.

  • approve

    Before an operation, users need to authorise the SakePerp contract to have access to the account funds.

  const tx = await quoteAsset.approve(sakePerpVaultAddr, constants.MaxUint256)
  await tx.wait()
  • addLiquidity (Add Liquidity)

    User (MM) will receive a certain amount of lpToken after adding liquidity.
    
    addLiquidity(
      IExchange _exchange,
      Risk _risk,
      Decimal.decimal memory _quoteAssetAmount
    )
    
    _exchange: Trading pool address
    _risk: Fixed as 0
    _quoteAssetAmount: the amount user wants to add
    
    const { parseUnits } = require("ethers/lib/utils")
    const DEFAULT_DECIMALS = 18
    const quoteAssetAmount = { d: parseUnits("10000", DEFAULT_DECIMALS) }
    const options = { gasLimit: 2_500_000 } 
    const tx = await sakePerpVault.addLiquidity(
      exchangeAddr,
      0,
      quoteAssetAmount,
    )
    await tx.wait()
  • removeLiquidity (Remove Liquidity)

    When the liquidity is removed, the user's lpToken will be destroyed, and the user's funds will be returned according to the profit and loss of the trading pool.
    
    removeLiquidity(
      IExchange _exchange,
      Risk _risk,
      Decimal.decimal memory _lpTokenAmount
    )
    
    _exchange: Trading pool address
    _risk: Fixed as 0
    _lpTokenAmount: The amount of LPToken which will be destoryed
    
    const { parseUnits } = require("ethers/lib/utils")
    const DEFAULT_DECIMALS = 18
    const lpTokenAmount = { d: parseUnits("100.111", DEFAULT_DECIMALS) }
    const options = { gasLimit: 2_500_000 } 
    const tx = await sakePerpVault.removeLiquidity(
      exchangeAddr,
      0,
      lpTokenAmount,
    )
    await tx.wait()
  • getTotalLpUnrealizedPNL (Get the total profit and loss of the trading pool)

    getTotalLpUnrealizedPNL(IExchange _exchange)
    
    _exchange: Trading pool address
  • getLpTokenPrice (Get the LPToken price of the trading pool)

    getLpTokenPrice(IExchange _exchange, Risk _risk)
    
    _exchange: Trading pool address
    _risk: Fixed as 0
  • getTotalMMLiquidity (Get the total liquidity of the trading pool)

    getTotalMMLiquidity(address _exchange)
    
    _exchange:Trading pool address
  • getTotalMMAvailableLiquidity (Get the available liquidity of the trading pool)

    To protect MM, only part of the funds invested by MM is available.
    getTotalMMAvailableLiquidity(address _exchange)
    
    _exchange:Trading pool address

Trading Pool Status Enquiry

The exchange contract provides an interface for querying status information of the trading pool.

  • getUnderlyingPrice(Return the oracle price, also known as indexPrice)

  • getUnderlyingTwapPrice(Return the time-weighted oracle price over a period of time)

  • getSpotPrice(Return the contract price, also known as markPrice)

  • getTwapPrice(Return the time-weighted contract price over a period of time)

  • getReserve(Return the x and y values of the vAMM curve)

  • getMaxHoldingBaseAsset(Return the maximum position size for a single user, a value of 0 means no limit)

  • initMarginRatio(Return the initial margin ratio, which determines the maximum leverage)

  • maintenanceMarginRatio(Return the maintenance margin ratio below which the position will be liquidated)

  • spreadRatio(Return to the spread)

  • liquidationFeeRatio(Return to the liquidation fee ratio)

  • maxLiquidationFee(Return to the max. liquidation fee)

  • getTotalPositionSize(Return to the net position of the trading pool)

  • getPositionSize(Return the size of long and short positions respectively)

Last updated