DODO V1/V2 集成指南
DODO V1/V2 合约地址#
合约地址链接:DODO V1/V2 合约地址
DODO V1/V2 主网 Subgraph 地址#
公式解释#
交易对中的交易代币称为 基础代币,定价代币称为 报价代币,分别简称为 和 。
DODO V2 的价格曲线公式如下:
其中, 代表参考价格, 为滑点因子, 表示当前的代币库存, 代表平衡状态的库存, 表示当前代币库存与平衡状态的偏差程度。该模型中,交易代币和定价代币是对称的,同样有如下公式:
将上述两个公式结合,我们得到 PMM 曲线的公式:
在给定的上下文中,变量 有三种情况:
- 如果 ,则 ,表示
- 如果 ,则 ,表示
- 其他情况下,
池子地址检索#
通过 Subgraph,用户可以检索所有池子的地址。用户可以选择池子类型来获取 V1 和 V2 的池子。
V2 包含三种类型的池子:DVM、DSP 和 DPP。
{
pairs(where: { type_in: ["DVM", "DSP", "DPP"] }) {
id
}
}
所有 V1 池子属于 CLASSICAL 类型。
{
pairs(where: { type: "CLASSICAL" }) {
id
}
}
计算指南#
-
DODO V2
在 V2 中,有两种交换方法:
sellBase
和sellQuote
。通过调用 Query 方法来计算相应的 receiveAmount 和交易费用。最终的 receiveAmount 会被交易费用所减少,并转移到用户账户。V2 包括三种类型的池子:DVM、DSP 和 DPP。这三种类型的池子都有sellBase
和sellQuote
的交换方法,并且它们的实现逻辑相似。我们将主要关注 DVM 池子的解释。DVMTrader 代码链接:https://github.com/DODOEX/contractV2/blob/main/contracts/DODOVendingMachine/impl/DVMTrader.sol
DVMTrader 中重要函数的源代码如下:
// ============ 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; (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; (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); 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); mtFee = DecimalMath.mulFloor(receiveBaseAmount, mtFeeRate); receiveBaseAmount = receiveBaseAmount .sub(DecimalMath.mulFloor(receiveBaseAmount, lpFeeRate)) .sub(mtFee); }
DPP 池子和 DSP 池子中的 Sell 方法实现与 DVM 池子基本相同。区别在于 Query 方法中,DVM 池子返回两个值 (receiveAmount, mtFee),而 DPP 池子和 DSP 池子返回四个值 (receiveQuoteAmount, mtFee, newRState, newBaseTarget)。同时,DPP 池子和 DSP 池子的 Sell 方法中会更新池子参数 RState 和 BASE_TARGET。
DPPTrader 代码链接:https://github.com/DODOEX/contractV2/blob/main/contracts/DODOPrivatePool/impl/DPPTrader.sol
DSPTrader 代码链接:https://github.com/DODOEX/contractV2/blob/main/contracts/DODOStablePool/impl/DSPTrader.sol
sellBase
函数代码段如下:(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); }
核心计算实现在 PMMPricing 中的
sellBaseToken
和sellQuoteToken
函数中,根据 R 的值分为三种情况:- R = 1
- R > 1
- R < 1
PMMPricing 源代码链接:https://github.com/DODOEX/contractV2/blob/main/contracts/lib/PMMPricing.sol
重要函数的源代码如下:
// ============== PMMPricing Functions =============== // ============ Buy & Sell Functions ============ function sellBaseToken(PMMState memory state, uint256 payBaseAmount) internal pure returns (uint256 receiveQuoteAmount, RState newR) { if (state.R == RState.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); if (payBaseAmount < backToOnePayBase) { receiveQuoteAmount = _RAboveSellBaseToken(state, payBaseAmount); newR = RState.ABOVE_ONE; if (receiveQuoteAmount > backToOneReceiveQuote) { receiveQuoteAmount = backToOneReceiveQuote; } } else if (payBaseAmount == backToOnePayBase) { receiveQuoteAmount = backToOneReceiveQuote; newR = RState.ONE; } else { receiveQuoteAmount = backToOneReceiveQuote.add( _ROneSellBaseToken(state, payBaseAmount.sub(backToOnePayBase)) ); newR = RState.BELOW_ONE; } } else { 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) {>>>END>>>
function buyBaseToken(
uint256 amount,
uint256 maxPayQuote,
bytes calldata data
) external tradeAllowed buyingAllowed gasPriceLimit preventReentrant returns (uint256) {
// 查询价格
(
uint256 payQuote,
uint256 lpFeeBase,
uint256 mtFeeBase,
Types.RStatus newRStatus,
uint256 newQuoteTarget,
uint256 newBaseTarget
) = _queryBuyBaseToken(amount);
require(payQuote <= maxPayQuote, "BUY_BASE_COST_TOO_MUCH");
// 结算资产
_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);
}
return payQuote;
}
以上代码是DODO V1的buyBaseToken
函数的源代码,该函数用于以报价代币购买基础代币。buyBaseToken
函数的实现逻辑与V2中的sellQuoteToken
函数反向,它根据用户期望获得的receiveBaseAmount计算payQuoteAmount.
V1版本的sellBaseToken函数与V2版本的sellBase函数实现逻辑相同,都以payBaseAmount作为参数,在Query函数中计算receiveQuoteAmount。
V1中关于dodo代币的价格查询代码是_queryBuyBaseToken函数,源代码如下:
function _queryBuyBaseToken(
uint256 amount
)
internal
view
returns (
uint256,
uint256,
uint256,
Types.RStatus,
uint256,
uint256
)
{
PMMState memory state = pmmState;
uint256 newBaseTarget = state.B.sub(amount);
(uint256 receiveQuoteAmount, uint256 newR) = _BASE_TO_QUOTE(amount, state);
swapState.RStatus = newR;
Types.RStatus newRStatus;
if (state.lpFeeRate > 0) {
uint256 lpFeeBase = amount.mulCeil(state.lpFeeRate);
newBaseTarget = newBaseTarget.sub(lpFeeBase);
}
if (state.mtFeeRate > 0) {
uint256 mtFeeBase = amount.mulCeil(state.mtFeeRate);
newBaseTarget = newBaseTarget.sub(mtFeeBase);
}
if (newBaseTarget == 0) {
newRStatus = Types.RStatus.ONE;
} else {
if (newR == Types.RStatus.ABOVE_ONE) {
uint256 backToOnePayBase = state.B.sub(state.B0);
if (backToOnePayBase.add(amount) >= newBaseTarget) {
newRStatus = Types.RStatus.ONE;
receiveQuoteAmount = receiveQuoteAmount.add(_BASE_QUOTE(state.B.sub(state.B0)));
} else {
newRStatus = Types.RStatus.ABOVE_ONE;
}
} else if (newR == Types.RStatus.BELOW_ONE) {
uint256 backToOnePayQuote = state.Q0.sub(state.Q);
if (backToOnePayQuote >= receiveQuoteAmount) {
newRStatus = Types.RStatus.ONE;
receiveQuoteAmount = _BASE_QUOTE(state.B.sub(state.B0)).add(receiveQuoteAmount);
} else {
newRStatus = Types.RStatus.BELOW_ONE;
}
} else {
assert(newR == Types.RStatus.ONE);
if (state.Q.sub(receiveQuoteAmount) <= state.Q0) {
newRStatus = Types.RStatus.ONE;
} else {
newRStatus = Types.RStatus.BELOW_ONE;
}
}
}
return (
receiveQuoteAmount,
lpFeeBase,
mtFeeBase,
newRStatus,
state.Q.sub(receiveQuoteAmount),
newBaseTarget
);
}
DODO V1主要调用了两个库:DODOMath库和DecimalMath库。这些库中包含了一些用于数学计算的函数,如解一元二次函数、一般积分等。这些函数主要用于计算DODO的交易价格。
DecimalMath库的链接:https://github.com/DODOEX/contractV2/blob/main/contracts/lib/DecimalMath.sol
DODOMath库的链接:https://github.com/DODOEX/contractV2/blob/main/contracts/lib/DODOMath.sol
以上是关于DODO V1的一些相关代码和库的介绍。
发射ChargeMaintainerFee(_MAINTAINER_, true, mtFeeBase);
}
// 更新TARGET
如果 (_TARGET_QUOTE_TOKEN_AMOUNT_ != newQuoteTarget) {
_TARGET_QUOTE_TOKEN_AMOUNT_ = newQuoteTarget;
}
如果 (_TARGET_BASE_TOKEN_AMOUNT_ != newBaseTarget) {
_TARGET_BASE_TOKEN_AMOUNT_ = newBaseTarget;
}
如果 (_R_STATUS_ != newRStatus) {
_R_STATUS_ = newRStatus;
}
_donateBaseToken(lpFeeBase);
发射BuyBaseToken(msg.sender, amount, payQuote);
return payQuote;
}
// ============ 查询函数 ============
function _queryBuyBaseToken(uint256 amount)
internal
view
returns (
uint256 payQuote,
uint256 lpFeeBase,
uint256 mtFeeBase,
Types.RStatus newRStatus,
uint256 newQuoteTarget,
uint256 newBaseTarget
) {
(newBaseTarget, newQuoteTarget) = getExpectedTarget();
// 从用户接收的数量中收取费用
lpFeeBase = DecimalMath.mul(amount, _LP_FEE_RATE_);
mtFeeBase = DecimalMath.mul(amount, _MT_FEE_RATE_);
uint256 buyBaseAmount = amount.add(lpFeeBase).add(mtFeeBase);
如果 (_R_STATUS_ == Types.RStatus.ONE) {
// 情况1: R=1
payQuote = _ROneBuyBaseToken(buyBaseAmount, newBaseTarget);
newRStatus = Types.RStatus.ABOVE_ONE;
} else 如果 (_R_STATUS_ == Types.RStatus.ABOVE_ONE) {
// 情况2: R>1
payQuote = _RAboveBuyBaseToken(buyBaseAmount, _BASE_BALANCE_, newBaseTarget);
newRStatus = Types.RStatus.ABOVE_ONE;
} else 如果 (_R_STATUS_ == Types.RStatus.BELOW_ONE) {
uint256 backToOnePayQuote = newQuoteTarget.sub(_QUOTE_BALANCE_);
uint256 backToOneReceiveBase = _BASE_BALANCE_.sub(newBaseTarget);
// 情况3: R<1
// 复杂情况,R状态可能会改变
如果 (buyBaseAmount < backToOneReceiveBase) {
// 情况3.1: R状态不改变
// 不需要检查payQuote,因为备用基础代币必须大于零
payQuote = _RBelowBuyBaseToken(buyBaseAmount, _QUOTE_BALANCE_, newQuoteTarget);
newRStatus = Types.RStatus.BELOW_ONE;
} else 如果 (buyBaseAmount == backToOneReceiveBase) {
// 情况3.2: R状态改变为ONE
payQuote = backToOnePayQuote;
newRStatus = Types.RStatus.ONE;
} else {
// 情况3.3: R状态改变为ABOVE_ONE
payQuote = backToOnePayQuote.add(
_ROneBuyBaseToken(buyBaseAmount.sub(backToOneReceiveBase), newBaseTarget)
);
newRStatus = Types.RStatus.ABOVE_ONE;
}
}
返回 (payQuote, lpFeeBase, mtFeeBase, newRStatus, newQuoteTarget, newBaseTarget);
}
PMM状态构造#
池子状态的参数储存在PMMState中,被用于Swap来计算用户获取到的代币数量。有两种方法来获取PMMState,如下所述:
- 使用
getPMMState
函数获取当前池子的PMMstate。 - 利用PMMHelper在一次性中获取池子的PMMState以及多个参数,例如代币地址、交易费用等。
PMMState结构定义如下:
struct PMMState {
uint256 i;
uint256 K;
uint256 B;
uint256 Q;
uint256 B0;
uint256 Q0;
RState R;
}
使用getPMMState
函数#
- i,K,B,Q,R都是池子状态的参数,可读取。
- B0,Q0在函数内部自动计算。
函数的源代码如下:
// ============ 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);
}
B0和Q0的计算细节如下:
以的情况为例,计算价格曲线P的积分,得到 :
以为例,即给定、、、、,求。
的计算公式推导如下:
整理成以为未知数的二次方程标准形式:
令;;
舍去负的根,得到为:
类似地,求解 :
交易费用#
在V2中,任何交易都会产生费用。费用分为两部分:流动性提供者的lpFee
和DODO的mtFee
:
实际接收到的代币数量需要减去lpFee
和mtFee
:
费用计算是在Query方法中执行的。通过池子合约可以获得lpFeeRate
和mtFeeRate
。
- 对于DODO V1,可以通过函数
_LP_FEE_RATE_()
获得lpFeeRate,可以通过函数_MT_FEE_RATE_()
获得mtFeeRate。 - 对于DODO V2,可以使用以下函数同时获得lpFeeRate和mtFeeRate。
function getUserFeeRate(address userAddr) public returns(uint256 lpFeeRate, uint256 mtFeeRate) {
}
PMMHelper的使用#
PMMHelper与每个链的DODOV1PmmHelper和DODOV2RouteHelper相对应。Helper的每个版本都定义了PairDetail结构来存储池子的PMMState和费用。它们还提供了getPairDetail
函数来获取PairDetail。除了PMMState之外,PMMHelper的返回结果还包括lpFeeRate和mtFeeRate。每个链的具体地址可以在合约地址文档中找到。
合约地址文档链接:here
PairDetail结构定义如下:
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通过提供池子的地址来检索目标池子的PairDetail。
DODOV1PmmHelper链接:https://github.com/DODOEX/contractV2/blob/main/contracts/SmartRoute/helper/DODOV1PmmHelper.sol
函数入口如下:
function getPairDetail(address pool) external view returns (PairDetail[] memory res) {
}
DODOV2RouteHelper允许综合查询V2中包含特定交易对的三种类型池子的信息。通过提供涉及交易对的代币地址来实现对PairDetail的检索。
DODOV2RouteHelper链接:https://github.com/DODOEX/contractV2/blob/main/contracts/SmartRoute/helper/DODOV2RouteHelper.sol
函数入口如下:
function getPairDetail(address token0,address token1,address userAddr) external view returns (PairDetail[] memory res){
}