Liquidity Parameter Structure
Through the above diagram, we can understand that there are several parameters required to determine the bid curve: bid price down, bid price up, bid amount, and an additional parameter k to determine the shape of the curve. For more information about k, please refer to the details of PMM algorithm.
To determine the ask curve, we need four parameters: ask price down, ask price up, ask amount, and ask k. Therefore, all the parameters required for one token are as follows:
bid price down, bid price up, kBid, bid amount
ask price down, ask price up, kAsk, ask amount
In order to save gas consumption, we store the decimal of the price separately.
Similarly, the decimal of the amount can also be stored separately.
In the new version, we have introduced a midPrice and a transaction fee parameter called swapFee. The swapFee is composed of lpFee set by the maker and mtFee charged by Dodo. swapFee = lpFee + mtFee, similar to DODO V2. The swapFee is represented as a percentage. Therefore, bid price up and ask price down are automatically generated based on midPrice and swapFee.
The formula for this is:
swapSpread = midPrice * swapFee
bid price up = midPrice - swapSpread
ask price down = midPrice + swapSpread
In order to optimize storage, we will refer to the offset between the ask price up and midPrice as a percentage, known as the ask up rate. Similarly, we will refer to the offset of bid price down as a percentage. Therefore, we have conversion formulas for ask price up and bid price down.
ask price up = midPrice * (1 + ask up rate)
bid price down = midPrice * ( 1 - bid price down)
In this structure, the default decimal for mid price is 18. For example, if the current price is 1642, then mid price = 1642 and mid price decimal = 18; if the current price is 0.0015, then mid price = 15 and mid price decimal = 14.
Therefore, a total of two parameters are required for both curves:
price param: midPrice, midPrice decimal, lpfee rate, ask up rate, bid down rate
amount param:ask amount, bid amount
curve param: kAsk, kBid
Among them, the units of lpFee rate, ask up rate, and bid down rate are 10000, which means that if lpFee rate = 1000, it is equivalent to lpFee rate = 10%.
The units for ask amount and bid amount are both set to 18 by default.
Parameter Storage Optimization#
For each currency, the following parameters are needed to determine the trading curve:
- mid price- 16 bit
- mid price decimal -8 bit
- lp fee rate - 8 bit
- ask up rate - 8 bit
- bid down rate - 8 bit
- ask k - 16 bit
- ask amount - 16 bit
- ask amount decimal - 8 bit
- bid k - 16 bit
- bid amount - 16 bit
- bid amount decimal - 8 bit
Please note that we only support 16-bit storage for price or amount, which means we do not support setting integers above 65535. If you need to set a number above 65535, you will need to convert it into decimal format. For example, if you want to set the actual ask amount as 400,000, you can set the contract parameters as ask_amount = 40000 and ask_amount_decimal = 19.
In the D3MM main body, the struct TokenMMInfo is used to store the relevant parameters of each token. However, in D3Maker, we will use a more compact structure called TokenMMInfoWithoutCum to store token-related parameters. In this struct, we employ bitwise operations to compress all price-related parameters into one uint and all amount-related parameters into another uint.
In the following text, we will refer to the data that contains all price information for a token as priceISet. Similarly, the data that contains all amount information for the same token will be referred to as amountSet.
The struct definition is as follows:
struct TokenMMInfoWithoutCum {
// [mid price(16) | mid price decimal(8) | fee rate(16) | ask up rate (16) | bid down rate(16)]
// midprice unit is 1e18
// all rate unit is 10000
uint80 priceInfo; // Why is it 80 instead of 72? In order to match the following few numbers and make a total of 256 for storage efficiency.
// [ask amounts(16) | ask amounts decimal(8) | bid amounts(16) | bid amounts decimal(8) ]
uint64 amountInfo; // Why is it 64 instead of 48? In order to match the following few numbers and make a total of 256 for storage efficiency.
// k is [0, 10000]
uint16 kAsk;
uint16 kBid;
uint8 decimal;
uint8 tokenIndex;
}
One price set contains 72 bits.
One amount set contains 48 bits.
It can be seen that, in this packaging method, all the data of a token price only requires 72 bits. In order to save gas for modifying multiple prices of SP, we have set up an additional array to package all the token price information. In this array, each uint256 stores the price information of 3 tokens.
At the same time, considering that stablecoins and non-stablecoins may have different frequency of price updates, separate arrays can be used to store stablecoins and non-stablecoins. SP can adjust them flexibly. Please note that "stablecoin" and "non-stablecoin" here are not mandatory bindings to the actual types of currencies. SP can set them according to their own needs when adding tokens for the first time. For example, if a pool only has three tokens - USDT, WETH, WBTC - and USDT's token setting frequency is similar to WETH's, you can declare USDT as a non-stablecoin during initial setup. The contract will pack the priceSet related to USDT together with those of WETH and WBTC into one slot, saving gas.
The specific structure of the price data is as follows:
// pack three token price in one slot
struct PriceListInfo {
// odd for non-stable, even for stable, true index = tokenIndex[address] / 2
mapping(address => uint256) tokenIndexMap;
uint256 numberOfNS; // quantity of not stable token
uint256 numberOfStable; // quantity of stable token
// [mid price(16) | mid price decimal(8) | fee rate(16) | ask up rate (16) | bid down rate(16)] = 72 bit
// one slot contain = 80 * 3, 3 token price
// [2 | 1 | 0]
uint256[] tokenPriceNS; // non-stable token price
uint256[] tokenPriceStable; // stable token price
}
Each token has an index, which can be obtained through the getOneTokenOriginIndex() function.
function getOneTokenOriginIndex(address token) public view returns (uint256)
OriginIndex is odd for non-stable coins, and even for stable coins. By dividing originIndex by 2, we get trueIndex. TrueIndex represents the index within the major category (stable/non-stable) of the coin. trueIndex divided by 3 gives us the position of the token's slot in the array, while trueIndex % 3 represents the sequence of the token within that slot.
/*
For example: 7 assets,WBTC,WETH,BNB,DODO,DAI,USDT,USDC
numberOfNS = 4
numberOfStable = 3
origin index in tokenIndexMap:
DAI 0
WBTC 1
USDT 2
WETH 3
USDC 4
BNB 5
DODO 7
trueIndex:
DAI 0,USDT 1, USDC 2
WBTC 0, WETH 1, BNB 2, DODO 3
trueIndex / 3 :
DAI 0, USDT 0, USDC 0
WBTC 0, WETH 0, BNB 0, DODO 1
trueIndex % 3 :
DAI 0, USDT 1, USDC 2
WBTC 0, WETH 1, BNB 2, DODO 0
Therefore
The structure of tokenPriceNS: [(BNB | WETH | WBTC), (empty | empty | DODO)]
The structure of tokenPriceStable: [(USDC | USDT | DAI)]
*/
D3Maker provides the following functions:
First-time tokenInfo setup:
- setNewToken
Setting initialized Token-related information:
- setTokensPrice
- setNSPriceSlot
- setStablePriceSlot
- setTokensAmounts
- setTokensKs
The functions will be explained one by one in the following text.
setNewToken#
Set the information for a new token, including: whether to define it as a stablecoin, and can only be called once when adding the token.
Example: (being updated)
/// @notice maker set a new token info
/// @param token token's address
/// @param priceSet packed price, [mid price(16) | mid price decimal(8) | fee rate(16) | ask up rate (16) | bid down rate(16)]
/// @param amountSet describe ask and bid amount and K, [ask amounts(16) | ask amounts decimal(8) | bid amounts(16) | bid amounts decimal(8) ] = one slot could contains 4 token info
/// @param stableOrNot describe this token is stable or not, true = stable coin
/// @param kAsk k of ask curve
/// @param kBid k of bid curve
/// @param tokenDecimal token's decimal
function setNewToken(
address token,
bool stableOrNot,
uint80 priceSet,
uint64 amountSet,
uint16 kAsk,
uint16 kBid,
uint8 tokenDecimal
) external onlyOwner {
}
setTokensPrice#
Set the price information for multiple tokens at the same time by passing in an array of tokens and their corresponding price sets. The contract will internally concatenate the token information into a single slot.
Example: (being updated)
/// @notice set token prices
/// @param tokens token address set
/// @param tokenPrices token prices set, each number pack one token all price.Each format is the same with priceSet
/// [mid price(16) | mid price decimal(8) | fee rate(16) | ask up rate (16) | bid down rate(16)] = one slot could contains 3 token info
function setTokensPrice(
address[] calldata tokens,
uint80[] calldata tokenPrices
) external onlyOwner {
setNSPriceSlot#
The most efficient way to set multiple non-stable coin token prices is by packaging the corresponding price sets into a single slot beforehand. In the contract, only the slot data needs to be overwritten. The slot index can be calculated based on the method mentioned above. The contract will not split and check the data when setting it, so SP needs to carefully determine the slot data (reverting any bad data during swapping).
For example, if we want to reset the prices of WBTC and WETH, since both WBTC and WETH belong to non-stable tokens in the tokenPriceNS storage, they are located in slot 0. Therefore, we need to call the setNSPriceSlot() method with [0] as the first parameter. The second parameter should be obtained by using getNSTokenInfo() to retrieve the current price data at position 0 in slot 0 and then overwrite it with new WBTC and WETH prices.
As for the third parameter, it should also be obtained through getNSTokenInfo(), retrieving allFlag first and then setting its binary representation's first bit (corresponding to WBTC) and third bit (corresponding to WETH) as 0 since their original indices are 1 and 3 respectively.
/// @notice user set PriceListInfo.tokenPriceStable price info, only for stable coin
/// @param slotIndex tokenPriceStable index
/// @param priceSlots tokenPriceNS price info, every data has packed all 3 token price info
/// @param newAllFlag maker update token cumulative status,
/// for allFlag, tokenOriIndex represent bit index in allFlag. eg: tokenA has origin index 3, that means (allFlag >> 3) & 1 = token3's flag
/// flag = 0 means to reset cumulative. flag = 1 means not to reset cumulative.
/// @dev maker should be responsible for data availability
function setNSPriceSlot(
uint256[] calldata slotIndex,
uint256[] calldata priceSlots,
uint256 newAllFlag
) external onlyOwner {
setStablePriceSlot#
Used for overriding the stablecoin slot, similar to setNSPriceSlot.
/// @notice user set PriceListInfo.tokenPriceStable price info, only for stable coin
/// @param slotIndex tokenPriceStable index
/// @param priceSlots tokenPriceStable price info, every data has packed all 3 token price info
/// @param newAllFlag maker update token cumulative status,
/// for allFlag, tokenOriIndex represent bit index in allFlag. eg: tokenA has origin index 3, that means (allFlag >> 3) & 1 = token3's flag
/// flag = 0 means to reset cumulative. flag = 1 means not to reset cumulative.
/// @dev maker should be responsible for data availability
function setStablePriceSlot(
uint256[] calldata slotIndex,
uint256[] calldata priceSlots,
uint256 newAllFlag
) external onlyOwner {
setTokensAmounts#
Set the amounts information for multiple tokens by passing in an array of tokens and their corresponding amountSet data.
For example: (being updated)
/// @notice set token Amounts
/// @param tokens token address set
/// @param tokenAmounts token amounts set, each number pack one token all amounts.Each format is the same with amountSetAndK
/// [ask amounts(16) | ask amounts decimal(8) | bid amounts(16) | bid amounts decimal(8) ] = one slot could contains 4 token info
function setTokensAmounts(
address[] calldata tokens,
uint64[] calldata tokenAmounts
) public onlyOwner
setTokensKs#
Set multiple tokens' k data and write to both kAsk and kBid. Combine the same kAsk and kBid into a 32-bit data structure: [kAsk(16) | kBid(16)].
Example: (being updated).
/// @notice set token Ks
/// @param tokens token address set
/// @param tokenKs token k_ask and k_bid, structure like [kAsk(16) | kBid(16)]
function setTokensKs(address[] calldata tokens, uint32[] calldata tokenKs) public onlyOwner
Other auxiliary function introduction#
- getStableTokenInfo, get the number of stablecoins, the array of price slots for stablecoins, and the current flag.
/// @notice get all stable token Info
/// @param numberOfStable stable tokens' quantity
/// @param tokenPriceStable stable tokens' price slot array. each data contains up to 3 token prices
function getStableTokenInfo() public view returns (
uint256 numberOfStable,
uint256[] memory tokenPriceStable
uint256 curFlag
)
- getNSTokenInfo, get the number of non-stablecoins, the array of price slots for stablecoins, and the current flag.
/// @notice get all non-stable token Info
/// @param number stable tokens' quantity
/// @param tokenPrices stable tokens' price slot array. each data contains up to 3 token prices
function getNSTokenInfo() public view returns (
uint256 number,
uint256[] memory tokenPrices,
uint256 curFlag
)
- stickPrice, to concatenate prices, given a complete price slot, the token to be set at the index in the slot, and the new priceSet for that token, form a new price slot.
/// @notice used for construct several price in one price slot
/// @param priceSlot origin price slot
/// @param slotInnerIndex token index in slot
/// @param priceSet the token info needed to update
function stickPrice(
uint256 priceSlot,
uint256 slotInnerIndex,
uint256 priceSet
) public pure returns (uint256 newPriceSlot)
- multicall, used to package multiple settings in a single transaction.
/// @notice maker could use multicall to set different params in one tx.
function multicall(bytes[] calldata data) public returns (bytes[] memory results)
- getOneTokenOriginIndex(), get the original index of the token, as described in the previous text.
/// @notice get one token index. odd for none-stable, even for stable, true index = tokenIndex[address] / 2
function getOneTokenOriginIndex(address token) public view returns (uint256)
Parameter Data Splicing#
Below are some examples of code for parameter splicing for reference by SP. Of course, SP can also implement their own method of parameter splicing to achieve higher efficiency and gas savings in market making.
// Concatenate four data into one slot
// A slot is (16 | 8 | 16 | 8)
// This method can be used to concatenate into an amountSet (ask amount | ask amount decimal | bid amount | bid amount decimal)
// Note that the return value is in 256 bits and needs to be converted.
function stickOneSlot(
uint256 numberA,
uint256 numberADecimal,
uint256 numberB,
uint256 numberBDecimal
) public pure returns (uint256 numberSet) {
numberSet =
(numberA << 32) +
(numberADecimal << 24) +
(numberB << 8) +
numberBDecimal;
}
Concatenate amount#
// Generate a standard 64-bit amountInfo.
function stickAmount(
uint256 askAmount,
uint256 askAmountDecimal,
uint256 bidAmount,
uint256 bidAmountDecimal
) public pure returns (uint64 amountSet) {
amountSet = uint64(
stickOneSlot(
askAmount,
askAmountDecimal,
bidAmount,
bidAmountDecimal
)
);
}
Concatenate price#
function stickPrice(
uint256 midPrice,
uint256 midPriceDecimal,
uint256 feeRate,
uint256 askUpRate,
uint256 bidDownRate
) public pure returns(uint80 priceInfo) {
priceInfo = uint80(
(midPrice << 56) + (midPriceDecimal << 48) + (feeRate << 32) + (askUpRate << 16) + bidDownRate
);
}
Concatenate k#
function stickKs(uint256 kAsk, uint256 kBid) public pure returns (uint32 kSet) {
kSet = uint32((kAsk << 16) + kBid);
}
Example#
In this example, we will set the price of WBTC. Let's assume that we want to distribute the liquidity of WBTC according to the following parameters:
The bid price range is 27940-27990.
The ask price range is 28010-28040.
The bid amount is 1000 USD (how much USD to buy tokens).
The ask amount is 0.1 WBTC (how many tokens to sell).
The mid-point price is set at 28000, with mtFee = 0.02%.
For the two price curves of token A, the bid price is vUSD / A and the ask price is A / vUSD.
swapFee = (28000 - 27990) / 28000 ≈ 0.035%
lpFee = swapFee - mtFee = 0.015%
Rounding off, lpFee = 0.02% as 2
Therefore, the actual
bid up price = 28000 * (1 -(0.02% + 0.02%)) = 27988.8
ask down price = 28000 * (1 +(0.02% + 0.02%)) = 28011.2
Calculate the ask up rate and bid down rate by analogy.
ask up rate = (28040 - 28000) / 28000 ≈ 0.14% as 14
bid down rate = (27940 - 28000) / 28000 ≈0.21% as 21
bid_amount = 1000, decimal = 18 (vUSD's decimal is 18)
ask_amount = 0.1, decimal = 18 (default decimal is 18)
Remove the decimal point and use it for the data stored in the contract.
mid price = 28000, mid price decimal = 18
ask up rate = 14, bid down rate = 21
bid_amount = 1000, decimal = 18
ask_amount = 1, decimal = 17
One price set contains 72 bits.
Therefore PriceSet is
[ 28000 | 18 | 14 | 21 ]
One amount set contains 48 bits.
Therefore AmountSet is
[ 1 | 17 | 1000 | 18 ]
Other Setting#
The current maxInterval is also managed by D3Maker, so there are the following setting functions:
/// @notice set acceptable setting interval, if setting gap > maxInterval, swap will revert.
function setHeartbeat(uint256 newMaxInterval) public onlyOwner {
}