DODO V3 Integration Guide
DODO V3 Deployed Contracts Addresses#
This page contains all v3 addresses.
DODO V3 Subgraph Addresses#
Formula Explanation and Calculation Instructions#
Regarding routing, each DODO V3 can be regarded as a multi-asset pool, with each asset independently quoted in USD. For each asset, there are two curves:
- bid curve (market maker buys,user sells)
- ask curve (market maker sells,user buys)
When a user requests to swap A token for B token, the actual price calculation is done as follows: swap A to VUSD + swap VUSD to B. VUSD is a virtual USD used solely for valuation purposes and does not represent an actual token.
In both of the above steps, the swap curves are PMM curves, and the parameters within the PMM curves can be determined based on the market maker's set parameters and trading volume. By substituting these parameters into the PMM formula, the calculations can be performed.
- For the ask curve of TokenA, TokenA → VUSD, the base is TokenA, and the quote is VUSD
- For the bid curve of TokenA, VUSD → TokenA, the base is VUSD, and the quote is TokenA
Furthermore, since the price upper and lower limits are set by the market maker, one side of the quoted tokens in either curve is the virtual token VUSD. Only real tokens can be bought or sold. Therefore, the curves always maintain R > 1, meaning B < B0. Consequently, there is no longer a need for Q, Q0, or related parameters, and the target does not need to be reset.
In summary, for any PMM in v3, the required parameters are i, B0, B, and k.
In v3, there are two swap methods: sellToken and buyToken. For the current routing version, we will focus only on the sellToken method.
Based on the above analysis, the actual calculation process is as follows:
fromToken → VUSD → toToken
- We use PMMPricing._querySellQuote for fromToken’s bid curve
- We use PMMPricing._querySellQuote for toToken’s ask curve
It is important to note that, in order to reduce precision loss, all prices and amounts are calculated using a decimal of 1e18 during the calculation process. The fromAmount will be adjusted to an 18-digit amount during initialization, and the resulting toAmount, which is also 18 digits, will be adjusted to the decimal amount of the toToken at the end.
Link to the trading code of the main pool:https://github.com/DODOEX/dodo-v3/blob/main/contracts/DODOV3MM/D3Pool/D3Trading.sol
Source code:
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);
}
From above, it can be seen that the core calculation is performed in the querySellTokens
function of the pmmRangeOrder
contract, which can be viewed in this link:https://github.com/DODOEX/dodo-v3/blob/main/contracts/DODOV3MM/lib/PMMRangeOrder.sol
Source code:
// 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 is:
tokenState.BLeft = askOrNot
? tokenState.BMaxAmount - tokenMMInfo.cumulativeAsk
: tokenState.BMaxAmount - tokenMMInfo.cumulativeBid;
- PMMPricing._querySellQuoteToken(https://github.com/DODOEX/dodo-v3/blob/main/contracts/DODOV3MM/lib/PMMPricing.sol
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 Construction#
The formula is very clear, so the question becomes: How to construct the pmmState for the bid curve of fromToken, and how to construct the pmmState for the ask curve of toToken.
Based on this, the four parameters are defined as follows:
- i = lowest price set by market maker, readable
- B0 = calculated based on i, amount (maximum sell quantity set by the market maker, readable), p_up (highest price set by the market maker, readable), and k.
- B = B0 - cumulativeAmount (current traded volume, readable)
- k = The k set by the market maker, readable
The derivation of the formula for B0 is:
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)