DODO 文档中心

DODO V3 集成指南

   

DODO V3 部署的合约地址#

本页面包含所有v3地址

   

DODO V3 子图地址#

查询URL
Arbhttps://api.studio.thegraph.com/proxy/46336/dodoex_d3mm_arbitrum/v0.0.2
Polygonhttps://api.studio.thegraph.com/query/46336/dodoex_d3mm_polygon/version/latest
ETHhttps://api.studio.thegraph.com/query/46336/dodoex_d3mm_eth/version/latest
BSChttps://api.thegraph.com/subgraphs/name/yongjun925/dodoex_d3mm_bsc
Avaxhttps://api.studio.thegraph.com/query/2860/dodoex_d3mm_avax/v0.0.2
Optimismhttps://api.studio.thegraph.com/query/2860/dodoex_d3mm_optimism/v0.0.2

   

公式解释和计算说明#

关于路由,每个DODO V3都可以看作是一个多资产池,每种资产在USD中独立标价。对于每种资产,都有两条曲线:

  • 买入曲线(做市商买入、用户卖出)
  • 卖出曲线(做市商卖出、用户买入)

当用户要求将A代币换成B代币时,实际的价格计算如下:A换成VUSD + VUSD换成B。这里的VUSD是一种仅用于估值目的的虚拟USD,不代表实际的代币。

在上述的两个步骤中,交换曲线都是PMM曲线,并且PMM曲线的参数可以基于做市商设置的参数和交易量来确定。通过将这些参数代入PMM公式中,即可进行计算。

  • 对于代币A的卖出曲线,代币A → VUSD,基准是代币A,计价是VUSD
  • 对于代币A的买入曲线,VUSD → 代币A,基准是VUSD,计价是代币A

此外,由于价格的上下限由做市商设置,每个曲线中标价的一方都是虚拟代币VUSD。只能买入或卖出真正的代币。因此,曲线始终保持R > 1,意味着B < B0。因此,不再需要Q、Q0或相关参数,也无需重设目标。

综上所述,在v3中,所需的参数包括i、B0、B和k。

在v3中,有两种交换方法:sellToken和buyToken。对于当前的路由版本,我们只关注sellToken方法。

根据以上分析,实际的计算过程如下:

fromToken → VUSD → toToken

  1. 我们使用PMMPricing._querySellQuote来计算fromToken的买入曲线
  2. 我们使用PMMPricing._querySellQuote来计算toToken的卖出曲线

需要注意的是,在计算过程中,为了降低精度损失,所有价格和金额都使用1e18的小数进行计算。fromAmount在初始化时会调整为18位小数的金额,而最终结果toAmount也是18位小数的金额,在最后会调整为toToken的实际小数金额。

主池的交易代码链接:https://github.com/DODOEX/dodo-v3/blob/main/contracts/DODOV3MM/D3Pool/D3Trading.sol

源代码:

function sellToken(
        address to,
        address fromToken,
        address toToken,
        uint256 fromAmount,
        uint256 minReceiveAmount,
        bytes calldata data
    ) external poolOngoing nonReentrant returns (uint256) {
        require(ID3Maker(state._MAKER_).checkHeartbeat(), Errors.HEARTBEAT_CHECK_FAIL);

        _updateCumulative(fromToken);
        _updateCumulative(toToken);

        (uint256 payFromAmount, uint256 receiveToAmount, uint256 vusdAmount, uint256 swapFee, uint256 mtFee) =
            querySellTokens(fromToken, toToken, fromAmount);
        require(receiveToAmount >= minReceiveAmount, Errors.MINRES_NOT_ENOUGH);

        _transferOut(to, toToken, receiveToAmount);

        // external call & swap callback
        IDODOSwapCallback(msg.sender).d3MMSwapCallBack(fromToken, fromAmount, data);
        // transfer mtFee to maintainer
        _transferOut(state._MAINTAINER_, toToken, mtFee);

        require(
            IERC20(fromToken).balanceOf(address(this)) - state.balances[fromToken] >= fromAmount,
            Errors.FROMAMOUNT_NOT_ENOUGH
        );

        // record swap
        uint256 toTokenDec = IERC20Metadata(toToken).decimals();
        _recordSwap(fromToken, toToken, vusdAmount, Types.parseRealAmount(receiveToAmount + swapFee, toTokenDec));
        require(checkSafe(), Errors.BELOW_IM_RATIO);

        emit Swap(to, fromToken, toToken, payFromAmount, receiveToAmount, swapFee, mtFee, 0);
        return receiveToAmount;
    }

		/// @notice user could query sellToken result deducted swapFee, assign fromAmount
    /// @return payFromAmount fromToken's amount = fromAmount
    /// @return receiveToAmount toToken's amount
    /// @return vusdAmount fromToken bid vusd
    /// @return swapFee dodo takes the fee
    function querySellTokens(
        address fromToken,
        address toToken,
        uint256 fromAmount
    ) public view returns (uint256 payFromAmount, uint256 receiveToAmount, uint256 vusdAmount, uint256 swapFee, uint256 mtFee) {
        require(fromAmount > 1000, Errors.AMOUNT_TOO_SMALL);
        Types.RangeOrderState memory D3State = getRangeOrderState(fromToken, toToken);

        {
        uint256 fromTokenDec = IERC20Metadata(fromToken).decimals();
        uint256 toTokenDec = IERC20Metadata(toToken).decimals();
        uint256 fromAmountWithDec18 = Types.parseRealAmount(fromAmount, fromTokenDec);
        uint256 receiveToAmountWithDec18;
        ( , receiveToAmountWithDec18, vusdAmount) =
            PMMRangeOrder.querySellTokens(D3State, fromToken, toToken, fromAmountWithDec18);

        receiveToAmount = Types.parseDec18Amount(receiveToAmountWithDec18, toTokenDec);
        payFromAmount = fromAmount;
        }

        receiveToAmount = receiveToAmount > state.balances[toToken] ? state.balances[toToken] : receiveToAmount;

        uint256 swapFeeRate = D3State.fromTokenMMInfo.swapFeeRate +  D3State.toTokenMMInfo.swapFeeRate;
        swapFee = DecimalMath.mulFloor(receiveToAmount, swapFeeRate);
        uint256 mtFeeRate = D3State.fromTokenMMInfo.mtFeeRate +  D3State.toTokenMMInfo.mtFeeRate;
        mtFee = DecimalMath.mulFloor(receiveToAmount, mtFeeRate);

        return (payFromAmount, receiveToAmount - swapFee, vusdAmount, swapFee, mtFee);
    }

从上述代码可以看出,核心计算是在pmmRangeOrder合约的querySellTokens函数中进行的,可以在此链接中查看:https://github.com/DODOEX/dodo-v3/blob/main/contracts/DODOV3MM/lib/PMMRangeOrder.sol

源代码:

	  	
    // use fromToken bid curve and toToken ask curve
    function querySellTokens(
        Types.RangeOrderState memory roState,
        address fromToken,
        address toToken,
        uint256 fromTokenAmount
    ) internal view returns (uint256 fromAmount, uint256 receiveToToken, uint256 vusdAmount) {
        // contruct fromToken state and swap to vUSD
        uint256 receiveVUSD;
        {
            PMMPricing.PMMState memory fromTokenState = _contructTokenState(roState, true, false);
            receiveVUSD = PMMPricing._querySellQuoteToken(fromTokenState, fromTokenAmount);

            receiveVUSD = receiveVUSD > fromTokenState.BLeft ? fromTokenState.BLeft : receiveVUSD;
        }

        // construct toToken state and swap from vUSD to toToken
        {
            PMMPricing.PMMState memory toTokenState = _contructTokenState(roState, false, true);
            receiveToToken = PMMPricing._querySellQuoteToken(toTokenState, receiveVUSD);

            receiveToToken = receiveToToken > toTokenState.BLeft ? toTokenState.BLeft : receiveToToken;
        }

        // oracle protect
        {
            uint256 oracleToAmount = ID3Oracle(roState.oracle).getMaxReceive(fromToken, toToken, fromTokenAmount);
            require(oracleToAmount >= receiveToToken, Errors.RO_ORACLE_PROTECTION);
        }
        return (fromTokenAmount, receiveToToken, receiveVUSD);
    }

BLeft是:

tokenState.BLeft = askOrNot
            ? tokenState.BMaxAmount - tokenMMInfo.cumulativeAsk
            : tokenState.BMaxAmount - tokenMMInfo.cumulativeBid;
function _querySellQuoteToken(PMMState memory state, uint256 payQuoteAmount)
        internal
        pure
        returns (uint256 receiveBaseAmount)
    {
        receiveBaseAmount = _RAboveSellQuoteToken(state, payQuoteAmount);
    }

function _RAboveSellQuoteToken(PMMState memory state, uint256 payQuoteAmount)
        internal
        pure
        returns (
            uint256 receiveBaseToken
        )
    {
        return
            DODOMath._SolveQuadraticFunctionForTrade(
                state.B0,
                state.B,
                payQuoteAmount,
                DecimalMath.reciprocalFloor(state.i),
                state.K
            );
    }

PMMState构造#

公式非常清晰,问题变成了如何为fromToken的买入曲线构造pmmState,以及如何为toToken的卖出曲线构造pmmState。

基于此,定义了以下四个参数:

  • i = 做市商设定的最低价格,可读
  • B0 = 根据i,amount(做市商设定的最大卖出数量,可读),p_up(做市商设定的最高价格,可读)和k计算得出
  • B = B0 - cumulativeAmount(当前交易量,可读)
  • k = 做市商设定的参数k,可读
The derivation of the formula for B0 is:

$$
P_{up} = i(ONE-k+k(\frac{B_{0}}{B_{0}-A})^{2}) 
$$

$$
\frac {P_{up} + i * k - ONE* i }{i*k} = (\frac{B_{0}}{B_{0}-A})^{2}
$$

$$
\sqrt{\frac{P_{up} + i * k - ONE * i}{i * k} }= (\frac{B_{0}}{B_{0}-A})
$$

$$
(B_{0}-A)\sqrt{\frac{P_{up} + i * k - ONE * i}{i * k} }= B_{0}
$$

$$
B_{0}= A + \frac{A}{\sqrt{\frac{P_{up} + i * k - ONE * i}{i * k} }-1}
$$

```jsx
B_up = i(ONE - k + k*(B0 / B0 - amount)^2), record amount = A
(B_up + i*k - ONE *i) / i*k = (B0 / B0 - A)^2

B0 = A + A / (sqrt((B_up + i*k - ONE*i) / i*k) - 1)

The corresponding code is:

    // P_up = i(1 - k + k*(B0 / B0 - amount)^2), record amount = A
    // (P_up + i*k - i) / i*k = (B0 / (B0 - A))^2
    // B0 = A + A / (sqrt((P_up + i*k - i) / i*k) - 1)
    // i = priceDown
    function _calB0WithPriceLimit(
        uint256 priceUp,
        uint256 k,
        uint256 i,
        uint256 amount
    ) internal pure returns (uint256 baseTarget) {
        // (P_up + i*k - i)
        // temp1 = PriceUp + DecimalMath.mul(i, k) - i
        // temp1 price

        // i*k
        // temp2 = DecimalMath.mul(i, k)
        // temp2 price

        // (P_up + i*k - i)/i*k
        // temp3 = DecimalMath(temp1, temp2)
        // temp3 ONE

        // temp4 = sqrt(temp3 * ONE)
        // temp4 ONE

        // temp5 = temp4 - ONE
        // temp5 ONE

        // B0 = amount + DecimalMath.div(amount, temp5)
        // B0 amount
        if (k == 0) {
            baseTarget = amount;
        } else {
            uint256 temp1 = priceUp * ONE + i * k - i * ONE;
            uint256 temp2 = i * k;
            uint256 temp3 = DecimalMath.div(temp1, temp2);
            uint256 temp5 = DecimalMath.sqrt(temp3) - ONE;
            require(temp5 > 0, Errors.RO_PRICE_DIFF_TOO_SMALL);
            baseTarget = amount + DecimalMath.div(amount, temp5);
        }
    }

   

Oracle Protection#

  • Call Oracle's priceSources and getPrice functions. The function parameters are both token addresses, to retrieve the following parameters for each token:
    • isWhiteListed, equivalent to isFeasible
    • priceTolerance (unit is 1e18)
    • priceDecimal
    • tokenDecimal
    • token price, referred to as tokenPrice
  • The formula for calculating maxReceive off the chain is:
DecimalMath.div(
                (fromAmount * getPrice(fromToken)) / getPrice(toToken),
                DecimalMath.mul(fromTlr, toTlr)
            );
//其中, fromTlr = fromtoken的priceTolerance, toTlr = toToken的priceTolerance

// DecimalMath.mul(a, b) = a * b / (10**18)
// DecimalMath.div(a, b) = a * (10**18) / b;
    

   

Fees#

In the new v3, a transaction will incur a fee, which is a certain percentage of the toToken. The fee is divided into two parts: lpFee, which is collected by the liquidity provider, and mtFee, which is collected by DODO. These fees are collectively recorded as swapFeeRate, where swapFee = lpFee + mtFee, and swapFeeRate = lpFeeRate + mtFeeRate. To facilitate adjustments by liquidity providers, the swapFee parameter is tied to each token. Each token has a swapFeeRate, and during the transaction process, the total fee rate for the transaction from token A to token B is calculated as the sum of token A's swapFeeRate and token B's swapFeeRate. The feeRate unit is 1e18, which means that if swapFeeRate = 1e17, the actual fee rate is 10%.

Based on the code in Part 1, in D3MM, the receiveAmount is obtained by comparing querySellTokens with the actual balance of the pool. The transaction fee collected is swapFee = receiveAmount * (swapFeeRate_A + swapFeeRate_B). The actual receiveAmount that the user can receive is receiveAmount' = receiveAmount - swapFee.

   

Collateral Rate Check#

For pools that borrow funds from the vault, there is a collateral ratio check when swapping. This check ensures that the pool's collateral ratio remains healthy after the transaction occurs.

Vault code:https://github.com/DODOEX/dodo-v3/tree/main/contracts/DODOV3MM/D3Vault

The original code for collateral ratio check:

require(checkSafe(), Errors.BELOW_IM_RATIO);

This check occurs in the vault because only the vault has a complete record of pool borrowings. The specific implementation is as follows:


function checkSafe(address pool) public view returns (bool) {
        return getCollateralRatio(pool) >  1e18 + IM;
}

function getCollateralRatio(address pool) public view returns (uint256) {
        uint256 collateral = 0;
        uint256 debt = 0;
        for (uint256 i; i < tokenList.length; i++) {
            address token = tokenList[i];
            AssetInfo storage info = assetInfo[token];

            (uint256 balance, uint256 borrows) = getBalanceAndBorrows(pool, token);
            uint256 price = ID3Oracle(_ORACLE_).getPrice(token);
            
            if (balance >= borrows) {
                collateral += min(balance - borrows, info.maxCollateralAmount).mul(info.collateralWeight).mul(price);
            } else {
                debt += (borrows - balance).mul(info.debtWeight).mul(price);
            }
        }
        return _ratioDiv(collateral, debt);
    }

function _ratioDiv(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0 && b == 0) {
            return 1e18;
        } else if (a == 0 && b != 0) {
            return 0;
        } else if (a != 0 && b == 0) {
            return type(uint256).max;
        } else {
            return a.div(b);
        }
    }

The core is evident in the "getBalanceAndBorrows" section of the code:

function getBalanceAndBorrows(address pool, address token) public view returns (uint256, uint256) {
        uint256 balance = ID3MM(pool).getTokenReserve(token);
        uint256 borrows = getPoolBorrowAmount(pool, token);
        return (balance, borrows);
    }

The swap process only changes the balance, not the borrows. When swapping from A to B, the balance of A token decreases and the balance of B token increases. Therefore, you can use the getBalanceAndBorrows function to retrieve the balance and borrows beforehand, and then construct checks based on the getCollateralRatio method.

   

Get Parameters#

After organizing the above process, for each token, the following parameters need to be read:

  • swapFeeRate
  • bid:
    • i_bid(p_down_bid)
    • p_up_bid
    • amount_bid
    • cumulativeBid
    • kBid
  • ask:
    • i_ask(p_down_ask)
    • p_up_ask
    • amount_ask
    • cumulativeAsk
    • kAsk

In v3, you can request the following two methods to retrieve all information about a token:

function getTokenMMPriceInfoForRead(
        address token
    )
        external
        view
        returns (uint256 askDownPrice, uint256 askUpPrice, uint256 bidDownPrice, uint256 bidUpPrice, uint256 swapFee)

function getTokenMMOtherInfoForRead(
        address token
    )
        external
        view
        returns (
            uint256 askAmount,
            uint256 bidAmount,
            uint256 kAsk,
            uint256 kBid,
            uint256 cumulativeAsk,
            uint256 cumulativeBid
        )

For edge condition evaluation, the parameters to be obtained are:

  • vault tokenlist

    // D3Vault.sol
    function getTokenList() external view returns (address[] memory) {
  • borrowing records

    // D3Vault.sol
    function getBalanceAndBorrows(address pool, address token) public view returns (uint256 balance, uint256 borrow)
  • The balance of each token in this pool (note that the tokens here may not necessarily be the same as the tokens in the token list of the vault).

    // D3MM.sol
    function getTokenReserve(address token) public view returns(uint256)

V3 pool also provides two read functions to obtain token list. These functions are available in the feature-version branch, which is the deployed version:https://github.com/DODOEX/dodo-v3/blob/feature-version/contracts/DODOV3MM/D3Pool/D3MM.sol

  • Get all token addresses for which the market maker has provided prices, i.e., all tokens that support price inquiries.

    function getPoolTokenlist() external view returns(address[] memory)
    
  • Get all token addresses that the market maker has deposited

    function getDepositedTokenList() external view returns (address[] memory)