DODO V1/V2 Integration Guide
DODO V1/V2 Contract Address#
Contract address link:DODO V1/V2 Contract Address
DODO V1/V2 Mainnet Subgraph Address#
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 and respectively.
The DODO V2 price curve formula is as follows:
represents the reference price, is the slippage factor, denotes the current token inventory, represents the balanced inventory, and 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:
Combining the two formulas mentioned above, we arrive at the PMM curve formula:
There are three scenarios for the variable in the given context:
- If , then , indicating
- If , then , indicating
- In other cases,
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
andsellQuote
. 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 havesellBase
andsellQuote
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
andsellQuoteToken
functions in PMMPricing, categorized based on the value of R into three situations:- R = 1
- R > 1
- 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
andbuyBaseToken
.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 thesellBase
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 thesellQuoteToken
function in V2. ThesellQuoteToken
function calculates the receiveBaseAmount that the user can obtain based on the payQuoteAmount. In contrast, thebuyBaseToken
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:
- Use the
getPMMState
function to obtain the current PMMstate of the pool. - 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 as an example, the integral of the price curve is calculated to obtain :
Taking as an example, i.e., given , , , , , solve for .
The calculation formula for is derived as follows:
Rearranging the equation into the standard form of a quadratic equation with as the unknown:
Let ; ;
After discarding the negative root, is obtained as:
Similarly, solving for :
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:
The actual amount of tokens received by the user, needs to subtract both lpFee
and 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){
}