DODO V3 集成指南
DODO V3 部署的合约地址#
本页面包含所有v3地址。
DODO V3 子图地址#
公式解释和计算说明#
关于路由,每个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
- 我们使用PMMPricing._querySellQuote来计算fromToken的买入曲线
- 我们使用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;
- 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构造#
公式非常清晰,问题变成了如何为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)