DODO V1/V2 Integration Guide

   

DODO V1/V2 Contract Address#

Contract address link:DODO V1/V2 Contract Address

   

DODO V1/V2 Mainnet Subgraph Address#

ChainQuery URL
ETHhttps://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
Arbhttps://api.thegraph.com/subgraphs/name/dodoex/dodoex-v2-arbitrum

   

Formula Explanation#

The transaction token in a trading pair is referred to as the Base Token, while the pricing token is called the Queto Token, abbreviated as BB and QQ respectively.

The DODO V2 price curve formula is as follows:

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

ii represents the reference price, kk is the slippage factor, B0B_0 denotes the current token inventory, BB represents the balanced inventory, and B0B\frac{B_0}{B} indicates the degree of deviation of the current token inventory from the balanced state. In this model, the transaction token and the pricing token are symmetric, and similarly, we obtain:

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

Combining the two formulas mentioned above, we arrive at the PMM curve formula:

P=iRP=iR

There are three scenarios for the variable RR in the given context:

  • If B<B0B < B_0, then R=1k+(B0B)2kR=1-k+(\frac{B_0}{B})^2k, indicating R>1R>1
  • If Q<Q0Q < Q_0, then R=1/(1k+(Q0Q)2k)R=1/(1-k+(\frac{Q_0}{Q})^2k), indicating R<1R<1
  • In other cases, R=1R=1

   

Pool Address Retrieval#

With subgraph, users can retrieve all pool addresses. User can select the pool type to obtain pools for both V1 and V2.

V2 comprises three types of pools: DVM, DSP, DPP.

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

All V1 pools are of the CLASSICAL type.

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

   

Calculation Instructions#

  • DODO V2

    In V2, there are two swap methods: sellBase and sellQuote. The Query method is invoked in the Sell method, using payAmount as input to calculate the corresponding receiveAmount and transaction fee. The final receiveAmount will be reduced by the fee portion and transferred to the user. V2 includes three types of pools: DVM, DSP, DPP. All three types of pools have sellBase and sellQuote swap methods, and their implementation logic is similar. We will primarily focus on explaining the DVM pool.

    DVMTrader code link: https://github.com/DODOEX/contractV2/blob/main/contracts/DODOVendingMachine/impl/DVMTrader.sol

    DVMTrader source code for important functions is as follows:

    // ============ 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;
    		// Obtain the amountOut
        (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;
    		// Obtain the amountOut
        (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);
    		// Calculate mtFee and lpFee
    		// The final receiveAmount needs to subtract mtFee and lpFee
    		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);
    		// Calculate mtFee and lpFee
    		// The final receiveAmount needs to subtract mtFee and lpFee
        mtFee = DecimalMath.mulFloor(receiveBaseAmount, mtFeeRate);
        receiveBaseAmount = receiveBaseAmount
            .sub(DecimalMath.mulFloor(receiveBaseAmount, lpFeeRate))
            .sub(mtFee);
    }

    The implementation of the Sell method in the DPP pool and DSP pool is essentially the same as the DVM pool. The difference lies in the Query function, where the DVM pool returns two values (receiveAmount, mtFee), while the DPP pool and DSP pool return four values (receiveQuoteAmount, mtFee, newRState, newBaseTarget). Simultaneously, the pool parameters RState and BASE_TARGET are updated in Sell methods of DPP pool and DSP pool.

    DPPTrader code link: https://github.com/DODOEX/contractV2/blob/main/contracts/DODOPrivatePool/impl/DPPTrader.sol

    DSPTrader code link: https://github.com/DODOEX/contractV2/blob/main/contracts/DODOStablePool/impl/DSPTrader.sol

    sellBase function snippet is as follows:

    (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);
    }

    The core calculations are implemented in the sellBaseToken and sellQuoteToken functions in PMMPricing, categorized based on the value of R into three situations:

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

    PMMPricing source code link: https://github.com/DODOEX/contractV2/blob/main/contracts/lib/PMMPricing.sol

    Important functions source code are as follows:

    // ============== PMMPricing Functions ===============
    
    // ============ Buy & Sell Functions ============
    
    function sellBaseToken(PMMState memory state, uint256 payBaseAmount)
        internal
        pure
        returns (uint256 receiveQuoteAmount, RState newR)
    {
        if (state.R == RState.ONE) {
            // case 1: R=1
            // R falls below 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);
            // case 2: R>1
            // complex case, R status depends on trading amount
            if (payBaseAmount < backToOnePayBase) {
                // case 2.1: R status do not change
                receiveQuoteAmount = _RAboveSellBaseToken(state, payBaseAmount);
                newR = RState.ABOVE_ONE;
                if (receiveQuoteAmount > backToOneReceiveQuote) {
                    // [Important corner case!] may enter this branch when some precision problem happens. And consequently contribute to negative spare quote amount
                    // to make sure spare quote>=0, mannually set receiveQuote=backToOneReceiveQuote
                    receiveQuoteAmount = backToOneReceiveQuote;
                }
            } else if (payBaseAmount == backToOnePayBase) {
                // case 2.2: R status changes to ONE
                receiveQuoteAmount = backToOneReceiveQuote;
                newR = RState.ONE;
            } else {
                // case 2.3: R status changes to BELOW_ONE
                receiveQuoteAmount = backToOneReceiveQuote.add(
                    _ROneSellBaseToken(state, payBaseAmount.sub(backToOnePayBase))
                );
                newR = RState.BELOW_ONE;
            }
        } else {
            // state.R == RState.BELOW_ONE
            // case 3: R<1
            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) {
            receiveBaseAmount = _RAboveSellQuoteToken(state, payQuoteAmount);
            newR = RState.ABOVE_ONE;
        } else {
            uint256 backToOnePayQuote = state.Q0.sub(state.Q);
            uint256 backToOneReceiveBase = state.B.sub(state.B0);
            if (payQuoteAmount < backToOnePayQuote) {
                receiveBaseAmount = _RBelowSellQuoteToken(state, payQuoteAmount);
                newR = RState.BELOW_ONE;
                if (receiveBaseAmount > backToOneReceiveBase) {
                    receiveBaseAmount = backToOneReceiveBase;
                }
            } else if (payQuoteAmount == backToOnePayQuote) {
                receiveBaseAmount = backToOneReceiveBase;
                newR = RState.ONE;
            } else {
                receiveBaseAmount = backToOneReceiveBase.add(
                    _ROneSellQuoteToken(state, payQuoteAmount.sub(backToOnePayQuote))
                );
                newR = RState.ABOVE_ONE;
            }
        }
    }
    
    // ============ R = 1 cases ============
    
    function _ROneSellBaseToken(PMMState memory state, uint256 payBaseAmount)
        internal
        pure
        returns (
            uint256 // receiveQuoteToken
        )
    {
        // in theory Q2 <= targetQuoteTokenAmount
        // however when amount is close to 0, precision problems may cause Q2 > targetQuoteTokenAmount
        return
            DODOMath._SolveQuadraticFunctionForTrade(
                state.Q0,
                state.Q0,
                payBaseAmount,
                state.i,
                state.K
            );
    }
    
    function _ROneSellQuoteToken(PMMState memory state, uint256 payQuoteAmount)
        internal
        pure
        returns (
            uint256 // receiveBaseToken
        )
    {
        return
            DODOMath._SolveQuadraticFunctionForTrade(
                state.B0,
                state.B0,
                payQuoteAmount,
                DecimalMath.reciprocalFloor(state.i),
                state.K
            );
    }
    
    // ============ R < 1 cases ============
    
    function _RBelowSellQuoteToken(PMMState memory state, uint256 payQuoteAmount)
        internal
        pure
        returns (
            uint256 // receiveBaseToken
        )
    {
        return
            DODOMath._GeneralIntegrate(
                state.Q0,
                state.Q.add(payQuoteAmount),
                state.Q,
                DecimalMath.reciprocalFloor(state.i),
                state.K
            );
    }
    
    function _RBelowSellBaseToken(PMMState memory state, uint256 payBaseAmount)
        internal
        pure
        returns (
            uint256 // receiveQuoteToken
        )
    {
        return
            DODOMath._SolveQuadraticFunctionForTrade(
                state.Q0,
                state.Q,
                payBaseAmount,
                state.i,
                state.K
            );
    }
    
    // ============ R > 1 cases ============
    
    function _RAboveSellBaseToken(PMMState memory state, uint256 payBaseAmount)
        internal
        pure
        returns (
            uint256 // receiveQuoteToken
        )
    {
        return
            DODOMath._GeneralIntegrate(
                state.B0,
                state.B.add(payBaseAmount),
                state.B,
                state.i,
                state.K
            );
    }
    
    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
            );
    }

    The specific calculations involve calls to the DODOMath library and the DecimalMath library.

    DecimalMath library link: https://github.com/DODOEX/contractV2/blob/main/contracts/lib/DecimalMath.sol

    DODOMath library link: https://github.com/DODOEX/contractV2/blob/main/contracts/lib/DODOMath.sol

    Important functions source code are as follows:

    // ============ DODOMath Helper functions ============
    
    /*
        Integrate dodo curve from V1 to V2
        require V0>=V1>=V2>0
        res = (1-k)i(V1-V2)+ikV0*V0(1/V2-1/V1)
        let V1-V2=delta
        res = i*delta*(1-k+k(V0^2/V1/V2))
    
        i is the price of V-res trading pair
    
        support k=1 & k=0 case
    
        [round down]
    */
    function _GeneralIntegrate(
        uint256 V0,
        uint256 V1,
        uint256 V2,
        uint256 i,
        uint256 k
    ) internal pure returns (uint256) {
        require(V0 > 0, "TARGET_IS_ZERO");
        uint256 fairAmount = i.mul(V1.sub(V2)); // i*delta
        if (k == 0) {
            return fairAmount.div(DecimalMath.ONE);
        }
        uint256 V0V0V1V2 = DecimalMath.divFloor(V0.mul(V0).div(V1), V2);
        uint256 penalty = DecimalMath.mulFloor(k, V0V0V1V2); // k(V0^2/V1/V2)
        return DecimalMath.ONE.sub(k).add(penalty).mul(fairAmount).div(DecimalMath.ONE2);
    }
    
    /*
        Follow the integration expression above, we have:
        i*deltaB = (Q2-Q1)*(1-k+kQ0^2/Q1/Q2)
        Given Q1 and deltaB, solve Q2
        This is a quadratic function and the standard version is
        aQ2^2 + bQ2 + c = 0, where
        a=1-k
        -b=(1-k)Q1-kQ0^2/Q1+i*deltaB
        c=-kQ0^2
        and Q2=(-b+sqrt(b^2+4(1-k)kQ0^2))/2(1-k)
        note: another root is negative, abondan
    
        if deltaBSig=true, then Q2>Q1, user sell Q and receive B
        if deltaBSig=false, then Q2<Q1, user sell B and receive Q
        return |Q1-Q2|
    
        as we only support sell amount as delta, the deltaB is always negative
        the input ideltaB is actually -ideltaB in the equation
    
        i is the price of delta-V trading pair
    
        support k=1 & k=0 case
    
        [round down]
    */
    function _SolveQuadraticFunctionForTrade(
        uint256 V0,
        uint256 V1,
        uint256 delta,
        uint256 i,
        uint256 k
    ) internal pure returns (uint256) {
        require(V0 > 0, "TARGET_IS_ZERO");
        if (delta == 0) {
            return 0;
        }
    
        if (k == 0) {
            return DecimalMath.mulFloor(i, delta) > V1 ? V1 : DecimalMath.mulFloor(i, delta);
        }
    
        if (k == DecimalMath.ONE) {
            // if k==1
            // Q2=Q1/(1+ideltaBQ1/Q0/Q0)
            // temp = ideltaBQ1/Q0/Q0
            // Q2 = Q1/(1+temp)
            // Q1-Q2 = Q1*(1-1/(1+temp)) = Q1*(temp/(1+temp))
            // uint256 temp = i.mul(delta).mul(V1).div(V0.mul(V0));
            uint256 temp;
            uint256 idelta = i.mul(delta);
            if (idelta == 0) {
                temp = 0;
            } else if ((idelta * V1) / idelta == V1) {
                temp = (idelta * V1).div(V0.mul(V0));
            } else {
                temp = delta.mul(V1).div(V0).mul(i).div(V0);
            }
            return V1.mul(temp).div(temp.add(DecimalMath.ONE));
        }
    
        // calculate -b value and sig
        // b = kQ0^2/Q1-i*deltaB-(1-k)Q1
        // part1 = (1-k)Q1 >=0
        // part2 = kQ0^2/Q1-i*deltaB >=0
        // bAbs = abs(part1-part2)
        // if part1>part2 => b is negative => bSig is false
        // if part2>part1 => b is positive => bSig is true
        uint256 part2 = k.mul(V0).div(V1).mul(V0).add(i.mul(delta)); // kQ0^2/Q1-i*deltaB
        uint256 bAbs = DecimalMath.ONE.sub(k).mul(V1); // (1-k)Q1
    
        bool bSig;
        if (bAbs >= part2) {
            bAbs = bAbs - part2;
            bSig = false;
        } else {
            bAbs = part2 - bAbs;
            bSig = true;
        }
        bAbs = bAbs.div(DecimalMath.ONE);
    
        // calculate sqrt
        uint256 squareRoot =
            DecimalMath.mulFloor(
                DecimalMath.ONE.sub(k).mul(4),
                DecimalMath.mulFloor(k, V0).mul(V0)
            ); // 4(1-k)kQ0^2
        squareRoot = bAbs.mul(bAbs).add(squareRoot).sqrt(); // sqrt(b*b+4(1-k)kQ0*Q0)
    
        // final res
        uint256 denominator = DecimalMath.ONE.sub(k).mul(2); // 2(1-k)
        uint256 numerator;
        if (bSig) {
            numerator = squareRoot.sub(bAbs);
        } else {
            numerator = bAbs.add(squareRoot);
        }
    
        uint256 V2 = DecimalMath.divCeil(numerator, denominator);
        if (V2 > V1) {
            return 0;
        } else {
            return V1 - V2;
        }
    }
  • DODO V1

    In V1, there are two types of swap methods: sellBaseToken and buyBaseToken.

    Trader code link: https://github.com/DODOEX/dodo-smart-contract/blob/master/contracts/impl/Trader.sol

    The sellBaseToken function in V1 has the same implementation logic as the sellBase function in V2. Both take payBaseAmount as a parameter and calculate the receiveQuoteAmount in the Query function.

    The difference from V2 is that the buyBaseToken function in V1 has the inverse logic of the sellQuoteToken function in V2. The sellQuoteToken function calculates the receiveBaseAmount that the user can obtain based on the payQuoteAmount. In contrast, the buyBaseToken function takes the desired receiveBaseAmount as a parameter to calculate payQuoteAmount.

    buyBaseToken function source code is as follows:

    function buyBaseToken(
        uint256 amount,
        uint256 maxPayQuote,
        bytes calldata data
    ) external tradeAllowed buyingAllowed gasPriceLimit preventReentrant returns (uint256) {
        // query price
        (
            uint256 payQuote,
            uint256 lpFeeBase,
            uint256 mtFeeBase,
            Types.RStatus newRStatus,
            uint256 newQuoteTarget,
            uint256 newBaseTarget
        ) = _queryBuyBaseToken(amount);
        require(payQuote <= maxPayQuote, "BUY_BASE_COST_TOO_MUCH");
    
        // settle assets
        _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);
            emit ChargeMaintainerFee(_MAINTAINER_, true, mtFeeBase);
        }
    
        // update TARGET
        if (_TARGET_QUOTE_TOKEN_AMOUNT_ != newQuoteTarget) {
            _TARGET_QUOTE_TOKEN_AMOUNT_ = newQuoteTarget;
        }
        if (_TARGET_BASE_TOKEN_AMOUNT_ != newBaseTarget) {
            _TARGET_BASE_TOKEN_AMOUNT_ = newBaseTarget;
        }
        if (_R_STATUS_ != newRStatus) {
            _R_STATUS_ = newRStatus;
        }
    
        _donateBaseToken(lpFeeBase);
        emit BuyBaseToken(msg.sender, amount, payQuote);
    
        return payQuote;
    }
    
    // ============ Query Functions ============
    
    function _queryBuyBaseToken(uint256 amount)
        internal
        view
        returns (
            uint256 payQuote,
            uint256 lpFeeBase,
            uint256 mtFeeBase,
            Types.RStatus newRStatus,
            uint256 newQuoteTarget,
            uint256 newBaseTarget
        ) {
        (newBaseTarget, newQuoteTarget) = getExpectedTarget();
    
        // charge fee from user receive amount
        lpFeeBase = DecimalMath.mul(amount, _LP_FEE_RATE_);
        mtFeeBase = DecimalMath.mul(amount, _MT_FEE_RATE_);
        uint256 buyBaseAmount = amount.add(lpFeeBase).add(mtFeeBase);
    
        if (_R_STATUS_ == Types.RStatus.ONE) {
            // case 1: R=1
            payQuote = _ROneBuyBaseToken(buyBaseAmount, newBaseTarget);
            newRStatus = Types.RStatus.ABOVE_ONE;
        } else if (_R_STATUS_ == Types.RStatus.ABOVE_ONE) {
            // case 2: R>1
            payQuote = _RAboveBuyBaseToken(buyBaseAmount, _BASE_BALANCE_, newBaseTarget);
            newRStatus = Types.RStatus.ABOVE_ONE;
        } else if (_R_STATUS_ == Types.RStatus.BELOW_ONE) {
            uint256 backToOnePayQuote = newQuoteTarget.sub(_QUOTE_BALANCE_);
            uint256 backToOneReceiveBase = _BASE_BALANCE_.sub(newBaseTarget);
            // case 3: R<1
            // complex case, R status may change
            if (buyBaseAmount < backToOneReceiveBase) {
                // case 3.1: R status do not change
                // no need to check payQuote because spare base token must be greater than zero
                payQuote = _RBelowBuyBaseToken(buyBaseAmount, _QUOTE_BALANCE_, newQuoteTarget);
                newRStatus = Types.RStatus.BELOW_ONE;
            } else if (buyBaseAmount == backToOneReceiveBase) {
                // case 3.2: R status changes to ONE
                payQuote = backToOnePayQuote;
                newRStatus = Types.RStatus.ONE;
            } else {
                // case 3.3: R status changes to ABOVE_ONE
                payQuote = backToOnePayQuote.add(
                    _ROneBuyBaseToken(buyBaseAmount.sub(backToOneReceiveBase), newBaseTarget)
                );
                newRStatus = Types.RStatus.ABOVE_ONE;
            }
        }
    
        return (payQuote, lpFeeBase, mtFeeBase, newRStatus, newQuoteTarget, newBaseTarget);
    }

   

PMMState Construction#

Pool state parameters are stored in PMMState, and it is used in the Swap to calculate the amount of tokens received by the user. There are two methods to obtain PMMState, explained below:

  1. Use the getPMMState function to obtain the current PMMstate of the pool.
  2. Utilize PMMHelper to obtain the pool's PMMState, as well as multiple parameters such as token addresses, transaction fees, etc., in one go.

PMMState structure definition is as follows:

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

 

Using the getPMMState Function#

  • i,K,B,Q,Rare all pool state parameters and are readable.
  • B0,Q0 are automatically calculated within the function.

The function source code is as follows:

// ============ 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);
}

The calculation details for B0 and Q0 are as follows:

Taking the case where B<B0B < B_0 as an example, the integral of the price curve PP is calculated to obtain Δ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}

Taking B0B_0 as an example, i.e., given ii, kk, BB, QQ, Q0Q_0, solve for B0B_0.

The calculation formula for B0B_0 is derived as follows:

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

Rearranging the equation into the standard form of a quadratic equation with B0B_0 as the unknown:

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

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

After discarding the negative root, B0B_0 is obtained as:

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})

Similarly, solving for Q0Q_0

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

   

Transaction Fees#

In V2, any transaction incurs fees. The fee is divided into two parts: lpFee collected by liquidity providers and mtFee collected by DODO:

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

The actual amount of tokens received by the user, needs to subtract both lpFee and mtFee:

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

The fee calculation is performed in the Query method. Both lpFeeRate and mtFeeRate can be obtained through the pool contract.

  • For DODO V1, lpFeeRate can be obtained through the function _LP_FEE_RATE_(), and mtFeeRate can be obtained through the function _MT_FEE_RATE_().
  • For DODO V2, use the following function to obtain lpFeeRate and mtFeeRate simultaneously.
function getUserFeeRate(address userAddr) public returns(uint256 lpFeeRate, uint256 mtFeeRate) {
}

   

PMMHelper Utilization#

PMMHelper corresponds to DODOV1PmmHelper and DODOV2RouteHelper for each chain. Both versions of the Helper define the PairDetail structure for storing the pool's PMMState and fees. They also provide the getPairDetail function for obtaining PairDetail. In addition to PMMState, the return results of PMMHelper include lpFeeRate and mtFeeRate. Specific addresses for each chain can be found in the contract address documentation.

Contract address documentation link: here

PairDetail structure definition is as follows:

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 retrieves the PairDetail of a target pool by providing the pool's address.

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

The function entry is as follows:

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

DODOV2RouteHelper allows for a comprehensive query of information for the three types of pools in V2 that contain a specific trading pair. This is achieved by providing the addresses of the tokens involved in the trading pair, resulting in the retrieval of the PairDetail.

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

The function entry is as follows:

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