DODO 文档中心

DODO V1/V2 集成指南

   

DODO V1/V2 合约地址#

合约地址链接:DODO V1/V2 合约地址

   

DODO V1/V2 主网 Subgraph 地址#

查询 URL
以太坊https://api.thegraph.com/subgraphs/name/dodoex/dodoex-v2
BSChttps://api.thegraph.com/subgraphs/name/dodoex/dodoex-v2-bsc
Polygonhttps://api.thegraph.com/subgraphs/name/dodoex/dodoex-v2-polygon
Arbitrumhttps://api.thegraph.com/subgraphs/name/dodoex/dodoex-v2-arbitrum

   

公式解释#

交易对中的交易代币称为 基础代币,定价代币称为 报价代币,分别简称为 BBQQ

DODO V2 的价格曲线公式如下:

P=i(1k+(B0B)2k)P=i(1-k+(\frac{B_0}{B})^2k)

其中,ii 代表参考价格,kk 为滑点因子,B0B_0 表示当前的代币库存,BB 代表平衡状态的库存,B0B\frac{B_0}{B} 表示当前代币库存与平衡状态的偏差程度。该模型中,交易代币和定价代币是对称的,同样有如下公式:

P=i/(1k+(Q0Q)2k)P=i/(1-k+(\frac{Q_0}{Q})^2k)

将上述两个公式结合,我们得到 PMM 曲线的公式:

P=iRP=iR

在给定的上下文中,变量 RR 有三种情况:

  • 如果 B<B0B < B_0,则 R=1k+(B0B)2kR=1-k+(\frac{B_0}{B})^2k,表示 R>1R>1
  • 如果 Q<Q0Q < Q_0,则 R=1/(1k+(Q0Q)2k)R=1/(1-k+(\frac{Q_0}{Q})^2k),表示 R<1R<1
  • 其他情况下, R=1R=1

   

池子地址检索#

通过 Subgraph,用户可以检索所有池子的地址。用户可以选择池子类型来获取 V1 和 V2 的池子。

V2 包含三种类型的池子:DVM、DSP 和 DPP。

{
  pairs(where: { type_in: ["DVM", "DSP", "DPP"] }) {
    id
  }
}

所有 V1 池子属于 CLASSICAL 类型。

{
  pairs(where: { type: "CLASSICAL" }) {
    id
  }
}

   

计算指南#

  • DODO V2

    在 V2 中,有两种交换方法:sellBasesellQuote。通过调用 Query 方法来计算相应的 receiveAmount 和交易费用。最终的 receiveAmount 会被交易费用所减少,并转移到用户账户。V2 包括三种类型的池子:DVM、DSP 和 DPP。这三种类型的池子都有 sellBasesellQuote 的交换方法,并且它们的实现逻辑相似。我们将主要关注 DVM 池子的解释。

    DVMTrader 代码链接:https://github.com/DODOEX/contractV2/blob/main/contracts/DODOVendingMachine/impl/DVMTrader.sol

    DVMTrader 中重要函数的源代码如下:

    // ============ DVMTrader Functions ============
    // ============ Sell Functions ============
    function sellBase(address to)
        external
        preventReentrant
        returns (uint256 receiveQuoteAmount)
    {
        uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
        uint256 baseInput = baseBalance.sub(uint256(_BASE_RESERVE_));
        uint256 mtFee;
        (receiveQuoteAmount, mtFee) = querySellBase(tx.origin, baseInput);
    
        _transferQuoteOut(to, receiveQuoteAmount);
        _transferQuoteOut(_MAINTAINER_, mtFee);
        _setReserve(baseBalance, _QUOTE_TOKEN_.balanceOf(address(this)));
    
        emit DODOSwap(
            address(_BASE_TOKEN_),
            address(_QUOTE_TOKEN_),
            baseInput,
            receiveQuoteAmount,
            msg.sender,
            to
        );
    }
    function sellQuote(address to)
        external
        preventReentrant
        returns (uint256 receiveBaseAmount)
    {
        uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));
        uint256 quoteInput = quoteBalance.sub(uint256(_QUOTE_RESERVE_));
        uint256 mtFee;
        (receiveBaseAmount, mtFee) = querySellQuote(tx.origin, quoteInput);
    
        _transferBaseOut(to, receiveBaseAmount);
        _transferBaseOut(_MAINTAINER_, mtFee);
        _setReserve(_BASE_TOKEN_.balanceOf(address(this)), quoteBalance);
    
        emit DODOSwap(
            address(_QUOTE_TOKEN_),
            address(_BASE_TOKEN_),
            quoteInput,
            receiveBaseAmount,
            msg.sender,
            to
        );
    }
    // ============ Query Functions ============
    function querySellBase(address trader, uint256 payBaseAmount)
        public
        view
        returns (uint256 receiveQuoteAmount, uint256 mtFee)
    {
        (receiveQuoteAmount, ) = PMMPricing.sellBaseToken(getPMMState(), payBaseAmount);
    
        uint256 lpFeeRate = _LP_FEE_RATE_;
        uint256 mtFeeRate = _MT_FEE_RATE_MODEL_.getFeeRate(trader);
        mtFee = DecimalMath.mulFloor(receiveQuoteAmount, mtFeeRate);
        receiveQuoteAmount = receiveQuoteAmount
            .sub(DecimalMath.mulFloor(receiveQuoteAmount, lpFeeRate))
            .sub(mtFee);
    }
    function querySellQuote(address trader, uint256 payQuoteAmount)
        public
        view
        returns (uint256 receiveBaseAmount, uint256 mtFee)
    {
        (receiveBaseAmount, ) = PMMPricing.sellQuoteToken(getPMMState(), payQuoteAmount);
    
        uint256 lpFeeRate = _LP_FEE_RATE_;
        uint256 mtFeeRate = _MT_FEE_RATE_MODEL_.getFeeRate(trader);
        mtFee = DecimalMath.mulFloor(receiveBaseAmount, mtFeeRate);
        receiveBaseAmount = receiveBaseAmount
            .sub(DecimalMath.mulFloor(receiveBaseAmount, lpFeeRate))
            .sub(mtFee);
    }

    DPP 池子和 DSP 池子中的 Sell 方法实现与 DVM 池子基本相同。区别在于 Query 方法中,DVM 池子返回两个值 (receiveAmount, mtFee),而 DPP 池子和 DSP 池子返回四个值 (receiveQuoteAmount, mtFee, newRState, newBaseTarget)。同时,DPP 池子和 DSP 池子的 Sell 方法中会更新池子参数 RState 和 BASE_TARGET。

    DPPTrader 代码链接:https://github.com/DODOEX/contractV2/blob/main/contracts/DODOPrivatePool/impl/DPPTrader.sol

    DSPTrader 代码链接:https://github.com/DODOEX/contractV2/blob/main/contracts/DODOStablePool/impl/DSPTrader.sol

    sellBase 函数代码段如下:

    (receiveQuoteAmount, mtFee, newRState, newBaseTarget) = querySellBase(tx.origin, baseInput);
    
    // update TARGET
    if (_RState_ != uint32(newRState)) {
        require(newBaseTarget <= uint112(-1),"OVERFLOW");
        _BASE_TARGET_ = uint112(newBaseTarget);
        _RState_ = uint32(newRState);
        emit RChange(newRState);
    }

    核心计算实现在 PMMPricing 中的 sellBaseTokensellQuoteToken 函数中,根据 R 的值分为三种情况:

    1. R = 1
    2. R > 1
    3. R < 1

    PMMPricing 源代码链接:https://github.com/DODOEX/contractV2/blob/main/contracts/lib/PMMPricing.sol

    重要函数的源代码如下:

    // ============== PMMPricing Functions ===============
    // ============ Buy & Sell Functions ============
    function sellBaseToken(PMMState memory state, uint256 payBaseAmount)
        internal
        pure
        returns (uint256 receiveQuoteAmount, RState newR)
    {
        if (state.R == RState.ONE) {
            receiveQuoteAmount = _ROneSellBaseToken(state, payBaseAmount);
            newR = RState.BELOW_ONE;
        } else if (state.R == RState.ABOVE_ONE) {
            uint256 backToOnePayBase = state.B0.sub(state.B);
            uint256 backToOneReceiveQuote = state.Q.sub(state.Q0);
            if (payBaseAmount < backToOnePayBase) {
                receiveQuoteAmount = _RAboveSellBaseToken(state, payBaseAmount);
                newR = RState.ABOVE_ONE;
                if (receiveQuoteAmount > backToOneReceiveQuote) {
                    receiveQuoteAmount = backToOneReceiveQuote;
                }
            } else if (payBaseAmount == backToOnePayBase) {
                receiveQuoteAmount = backToOneReceiveQuote;
                newR = RState.ONE;
            } else {
                receiveQuoteAmount = backToOneReceiveQuote.add(
                    _ROneSellBaseToken(state, payBaseAmount.sub(backToOnePayBase))
                );
                newR = RState.BELOW_ONE;
            }
        } else {
            receiveQuoteAmount = _RBelowSellBaseToken(state, payBaseAmount);
            newR = RState.BELOW_ONE;
        }
    }
    function sellQuoteToken(PMMState memory state, uint256 payQuoteAmount)
        internal
        pure
        returns (uint256 receiveBaseAmount, RState newR)
    {
        if (state.R == RState.ONE) {
            receiveBaseAmount = _ROneSellQuoteToken(state, payQuoteAmount);
            newR = RState.ABOVE_ONE;
        } else if (state.R == RState.ABOVE_ONE) {>>>END>>>
function buyBaseToken(
      uint256 amount,
      uint256 maxPayQuote,
      bytes calldata data
  ) external tradeAllowed buyingAllowed gasPriceLimit preventReentrant returns (uint256) {
      // 查询价格
      (
          uint256 payQuote,
          uint256 lpFeeBase,
          uint256 mtFeeBase,
          Types.RStatus newRStatus,
          uint256 newQuoteTarget,
          uint256 newBaseTarget
      ) = _queryBuyBaseToken(amount);
      require(payQuote <= maxPayQuote, "BUY_BASE_COST_TOO_MUCH");

      // 结算资产
      _baseTokenTransferOut(msg.sender, amount);
      if (data.length > 0) {
          IDODOCallee(msg.sender).dodoCall(true, amount, payQuote, data);
      }
      _quoteTokenTransferIn(msg.sender, payQuote);
      if (mtFeeBase != 0) {
          _baseTokenTransferOut(_MAINTAINER_, mtFeeBase);
      }
      return payQuote;
  }

以上代码是DODO V1的buyBaseToken函数的源代码,该函数用于以报价代币购买基础代币。buyBaseToken函数的实现逻辑与V2中的sellQuoteToken函数反向,它根据用户期望获得的receiveBaseAmount计算payQuoteAmount.

V1版本的sellBaseToken函数与V2版本的sellBase函数实现逻辑相同,都以payBaseAmount作为参数,在Query函数中计算receiveQuoteAmount。

V1中关于dodo代币的价格查询代码是_queryBuyBaseToken函数,源代码如下:

function _queryBuyBaseToken(
    uint256 amount
)
    internal
    view
    returns (
        uint256,
        uint256,
        uint256,
        Types.RStatus,
        uint256,
        uint256
    )
{
    PMMState memory state = pmmState;

    uint256 newBaseTarget = state.B.sub(amount);
    (uint256 receiveQuoteAmount, uint256 newR) = _BASE_TO_QUOTE(amount, state);
    swapState.RStatus = newR;

    Types.RStatus newRStatus;

    if (state.lpFeeRate > 0) {
        uint256 lpFeeBase = amount.mulCeil(state.lpFeeRate);
        newBaseTarget = newBaseTarget.sub(lpFeeBase);
    }

    if (state.mtFeeRate > 0) {
        uint256 mtFeeBase = amount.mulCeil(state.mtFeeRate);
        newBaseTarget = newBaseTarget.sub(mtFeeBase);
    }
    if (newBaseTarget == 0) {
        newRStatus = Types.RStatus.ONE;
    } else {
        if (newR == Types.RStatus.ABOVE_ONE) {
            uint256 backToOnePayBase = state.B.sub(state.B0);
            if (backToOnePayBase.add(amount) >= newBaseTarget) {
                newRStatus = Types.RStatus.ONE;
                receiveQuoteAmount = receiveQuoteAmount.add(_BASE_QUOTE(state.B.sub(state.B0)));
            } else {
                newRStatus = Types.RStatus.ABOVE_ONE;
            }
        } else if (newR == Types.RStatus.BELOW_ONE) {
            uint256 backToOnePayQuote = state.Q0.sub(state.Q);
            if (backToOnePayQuote >= receiveQuoteAmount) {
                newRStatus = Types.RStatus.ONE;
                receiveQuoteAmount = _BASE_QUOTE(state.B.sub(state.B0)).add(receiveQuoteAmount);
            } else {
                newRStatus = Types.RStatus.BELOW_ONE;
            }
        } else {
            assert(newR == Types.RStatus.ONE);
            if (state.Q.sub(receiveQuoteAmount) <= state.Q0) {
                newRStatus = Types.RStatus.ONE;
            } else {
                newRStatus = Types.RStatus.BELOW_ONE;
            }
        }
    }
    return (
        receiveQuoteAmount,
        lpFeeBase,
        mtFeeBase,
        newRStatus,
        state.Q.sub(receiveQuoteAmount),
        newBaseTarget
    );
}

DODO V1主要调用了两个库:DODOMath库和DecimalMath库。这些库中包含了一些用于数学计算的函数,如解一元二次函数、一般积分等。这些函数主要用于计算DODO的交易价格。

DecimalMath库的链接:https://github.com/DODOEX/contractV2/blob/main/contracts/lib/DecimalMath.sol

DODOMath库的链接:https://github.com/DODOEX/contractV2/blob/main/contracts/lib/DODOMath.sol

以上是关于DODO V1的一些相关代码和库的介绍。

          发射ChargeMaintainerFee(_MAINTAINER_, true, mtFeeBase);
        }
      
        // 更新TARGET
        如果 (_TARGET_QUOTE_TOKEN_AMOUNT_ != newQuoteTarget) {
            _TARGET_QUOTE_TOKEN_AMOUNT_ = newQuoteTarget;
        }
        如果 (_TARGET_BASE_TOKEN_AMOUNT_ != newBaseTarget) {
            _TARGET_BASE_TOKEN_AMOUNT_ = newBaseTarget;
        }
        如果 (_R_STATUS_ != newRStatus) {
            _R_STATUS_ = newRStatus;
        }
      
        _donateBaseToken(lpFeeBase);
        发射BuyBaseToken(msg.sender, amount, payQuote);
      
        return payQuote;
    }
  
    // ============ 查询函数 ============
  
    function _queryBuyBaseToken(uint256 amount)
        internal
        view
        returns (
            uint256 payQuote,
            uint256 lpFeeBase,
            uint256 mtFeeBase,
            Types.RStatus newRStatus,
            uint256 newQuoteTarget,
            uint256 newBaseTarget
        ) {
        (newBaseTarget, newQuoteTarget) = getExpectedTarget();
      
        // 从用户接收的数量中收取费用
        lpFeeBase = DecimalMath.mul(amount, _LP_FEE_RATE_);
        mtFeeBase = DecimalMath.mul(amount, _MT_FEE_RATE_);
        uint256 buyBaseAmount = amount.add(lpFeeBase).add(mtFeeBase);
      
        如果 (_R_STATUS_ == Types.RStatus.ONE) {
            // 情况1: R=1
            payQuote = _ROneBuyBaseToken(buyBaseAmount, newBaseTarget);
            newRStatus = Types.RStatus.ABOVE_ONE;
        } else 如果 (_R_STATUS_ == Types.RStatus.ABOVE_ONE) {
            // 情况2: R>1
            payQuote = _RAboveBuyBaseToken(buyBaseAmount, _BASE_BALANCE_, newBaseTarget);
            newRStatus = Types.RStatus.ABOVE_ONE;
        } else 如果 (_R_STATUS_ == Types.RStatus.BELOW_ONE) {
            uint256 backToOnePayQuote = newQuoteTarget.sub(_QUOTE_BALANCE_);
            uint256 backToOneReceiveBase = _BASE_BALANCE_.sub(newBaseTarget);
            // 情况3: R<1
            // 复杂情况,R状态可能会改变
            如果 (buyBaseAmount < backToOneReceiveBase) {
                // 情况3.1: R状态不改变
                // 不需要检查payQuote,因为备用基础代币必须大于零
                payQuote = _RBelowBuyBaseToken(buyBaseAmount, _QUOTE_BALANCE_, newQuoteTarget);
                newRStatus = Types.RStatus.BELOW_ONE;
            } else 如果 (buyBaseAmount == backToOneReceiveBase) {
                // 情况3.2: R状态改变为ONE
                payQuote = backToOnePayQuote;
                newRStatus = Types.RStatus.ONE;
            } else {
                // 情况3.3: R状态改变为ABOVE_ONE
                payQuote = backToOnePayQuote.add(
                    _ROneBuyBaseToken(buyBaseAmount.sub(backToOneReceiveBase), newBaseTarget)
                );
                newRStatus = Types.RStatus.ABOVE_ONE;
            }
        }
      
        返回 (payQuote, lpFeeBase, mtFeeBase, newRStatus, newQuoteTarget, newBaseTarget);
    }

   

PMM状态构造#

池子状态的参数储存在PMMState中,被用于Swap来计算用户获取到的代币数量。有两种方法来获取PMMState,如下所述:

  1. 使用getPMMState函数获取当前池子的PMMstate。
  2. 利用PMMHelper在一次性中获取池子的PMMState以及多个参数,例如代币地址、交易费用等。

PMMState结构定义如下:

struct PMMState {
    uint256 i;
    uint256 K;
    uint256 B;
    uint256 Q;
    uint256 B0;
    uint256 Q0;
    RState R;
}

 

使用getPMMState函数#

  • i,K,B,Q,R都是池子状态的参数,可读取。
  • B0,Q0在函数内部自动计算。

函数的源代码如下:

// ============ Helper Functions ============
function getPMMState() public view returns (PMMPricing.PMMState memory state) {
    state.i = _I_;
    state.K = _K_;
    state.B = _BASE_RESERVE_;
    state.Q = _QUOTE_RESERVE_;
    state.B0 = _BASE_TARGET_;
    state.Q0 = _QUOTE_TARGET_;
    state.R = PMMPricing.RState(_RState_);
    PMMPricing.adjustedTarget(state);
}

B0和Q0的计算细节如下:

B<B0B < B_0的情况为例,计算价格曲线P的积分,得到ΔQ\Delta Q

ΔQ=B1B2i(1k+(B0B)2k)dB=i(B2B1)(1k+kB02B1B2)\begin{split} \Delta Q& =\int_{B_1}^{B_2}i(1-k+(\frac{B_0}{B})^2k) \mathrm{d}B\\ &=i(B_2-B_1)*(1-k+k\frac{B_0^2}{B_1B_2})\\ \end{split}

B0B_0为例,即给定iikkBBQQQ0Q_0,求B0B_0

B0B_0的计算公式推导如下:

QQ0=ΔQ=i(B0B)(1k+kB02BB0)Q-Q_0 =\Delta Q =i(B_0-B)*(1-k+k\frac{B_0^2}{BB_0})

整理成以B0B_0为未知数的二次方程标准形式:

kBB02+(12k)B0[(1k)B+ΔQi]=0\frac{k}{B}B_0^2+(1-2k)B_0-[(1-k)B+\frac{\Delta Q}{i}]=0

a=kBa=\frac{k}{B}b=(12k)b=(1-2k)c=[(1k)B+ΔQi]c=[(1-k)B+\frac{\Delta Q}{i}]

舍去负的根,得到B0B_0为:

B0=b+b24ac2a=B(1+(1+4kΔQiB1)2k)B_0 =\frac{-b+\sqrt{b^2-4ac}}{2a} =B*(1+\frac{(\sqrt{1+\frac{4k\Delta Q}{iB}}-1)}{2k})

类似地,求解Q0Q_0

Q0=Q(1+(1+4kΔBiQ1)2k)Q_0=Q*(1+\frac{(\sqrt{1+\frac{4k\Delta B}{iQ}}-1)}{2k})

   

交易费用#

在V2中,任何交易都会产生费用。费用分为两部分:流动性提供者的lpFee和DODO的mtFee

lpFee=receiveAmount×lpFeeRatemtFee=receiveAmount×mtFeeRate{\rm lpFee=receiveAmount×lpFeeRate} \\ {\rm mtFee=receiveAmount×mtFeeRate }

实际接收到的代币数量需要减去lpFeemtFee

receiveAmount=receiveAmountlpFeemtFee\rm receiveAmount = receiveAmount - lpFee - mtFee

费用计算是在Query方法中执行的。通过池子合约可以获得lpFeeRatemtFeeRate

  • 对于DODO V1,可以通过函数_LP_FEE_RATE_()获得lpFeeRate,可以通过函数_MT_FEE_RATE_()获得mtFeeRate。
  • 对于DODO V2,可以使用以下函数同时获得lpFeeRate和mtFeeRate。
function getUserFeeRate(address userAddr) public returns(uint256 lpFeeRate, uint256 mtFeeRate) {
}

   

PMMHelper的使用#

PMMHelper与每个链的DODOV1PmmHelper和DODOV2RouteHelper相对应。Helper的每个版本都定义了PairDetail结构来存储池子的PMMState和费用。它们还提供了getPairDetail函数来获取PairDetail。除了PMMState之外,PMMHelper的返回结果还包括lpFeeRate和mtFeeRate。每个链的具体地址可以在合约地址文档中找到。

合约地址文档链接:here

PairDetail结构定义如下:

struct PairDetail {
        uint256 i;
        uint256 K;
        uint256 B;
        uint256 Q;
        uint256 B0;
        uint256 Q0;
        uint256 R;
        uint256 lpFeeRate;
        uint256 mtFeeRate;
        address baseToken;
        address quoteToken;
        address curPair;
        uint256 pairVersion;
    }

DODOV1PmmHelper通过提供池子的地址来检索目标池子的PairDetail。

DODOV1PmmHelper链接:https://github.com/DODOEX/contractV2/blob/main/contracts/SmartRoute/helper/DODOV1PmmHelper.sol

函数入口如下:

function getPairDetail(address pool) external view returns (PairDetail[] memory res) {
}

DODOV2RouteHelper允许综合查询V2中包含特定交易对的三种类型池子的信息。通过提供涉及交易对的代币地址来实现对PairDetail的检索。

DODOV2RouteHelper链接:https://github.com/DODOEX/contractV2/blob/main/contracts/SmartRoute/helper/DODOV2RouteHelper.sol

函数入口如下:

function getPairDetail(address token0,address token1,address userAddr) external view returns (PairDetail[] memory res){
}