NFT AMM of SudoSwap LSSVM
0xstan & 0xmc
sudoAMM 代码解析
SudoSwap 实现的 sudoAMM,是一种 NFT AMM(通常所说的 AMM 是指 ERC20 token 的自动交易池,而 SudoSwap 实现的是可以交易 NFT 的自动交易池)具备体积小巧、节省 gas 的特点,用于使用可定制的绑定曲线促进 NFT (ERC721s) 到代币 (ETH 或 ERC20) 的交易。流动性提供者 (LP) 可以存入单边买卖池,或向双方提供点差以获取费用
sudoAMM 是基于 Uniswap 架构改进,为了适配 NFT 的交易定价,将恒定乘积定价公式改为 Linear 和 Exponential 两种定价模型。
SudoAMM's Architecture
大部分用户 (caller) 会使用 Router (LSSVMRouter
合约) 来与 sudoAMM 交互。
交易方用户需要传入交易池地址来指定要交易的交易对,当然用户也无需记忆交易池的地址信息,因为 sudoSwap 实现了链下的 indexer,来将 NFT/TOKEN 地址自动对应上交易池地址,用户在前端界面只需要选择对应的输入和输出资产即可自动选择相应的交易对地址。
流动性提供者 (Liquidity Providers) 使用 Factory (LSSVMPairFactory
合约) 为特定的 NFT 部署交易对 LSSVMPair
。
LSSVMPairs 有两种类型 LSSVMPairEnumerable
和 LSSVMPairMissingEnumerable
,具体使用哪一种取决于该交易对的 ERC721 合约是否支持 Enumerable (Enumerable 是一种 ERC721 的拓展标准,该标准允许使用序号来查看 NFT ID,以及可以查看某个用户名下的 NFT)。若已实现了 Enumerable 使用 LSSVMPairEnumerable
模板, 如果没有,则使用 LSSVMPairMissingEnumerable
模板,该模板中实现 ID 集合,以便用户轻松访问池中的 NFT ID。
对于实际的代币,NFT 与 ERC20 或 ETH 配对,其中 Missing Enumerable
是不支持 Enumerable 标准的 NFT;而与 Uniswap 类似, ETH/ERC20 需要做区分,因为其转账逻辑不同,需要分开两个函数实现。
因此有 4 种配对类型:
- Missing Enumerable - ETH
- Missing Enumerable - ERC20
- Enumerable - ETH
- Enumerable - ERC20
鉴于此 SudoSwap 给出了 4 种 LSSVMPair
创建模板:
- LSSVMPairEnumerableETH
- LSSVMPairEnumerableERC20
- LSSVMPairMissingEnumerableETH
- LSSVMPairMissingEnumerableERC20
Router
交易路由,用户可以使用 Router (LSSVMRouter
合约) 进行跨交易池批量交易。根据交易的输入和输出不同,有以下几种接口:
Method Name | Input & Output | Output NFT IDs |
---|---|---|
swapETHForAnyNFTs | 输入 ETH 输出 NFT | 按顺序购买 |
swapETHForSpecificNFTs | 输入 ETH 输出 NFT | 指定 ID 购买 |
swapNFTsForAnyNFTsThroughETH | 以 ETH 为媒介,输入 NFT(A) 输出 NFT(B) | 按顺序购买 |
swapNFTsForSpecificNFTsThroughETH | 以 ETH 为媒介,输入 NFT(A) 输出 NFT(B) | 指定 ID 购买 |
swapERC20ForAnyNFTs | 输入 ERC20 输出 NFT | 按顺序购买 |
swapERC20ForSpecificNFTs | 输入 ERC20 输出 NFT | 指定 ID 购买 |
swapNFTsForToken | 输入 NFT 输出 ETH/ERC20 | 按顺序购买 |
swapNFTsForAnyNFTsThroughERC20 | 以 ERC20 为媒介,输入 NFT(A) 输出 NFT(B) | 按顺序购买 |
swapNFTsForSpecificNFTsThroughERC20 | 以 ERC20 为媒介,输入 NFT(A) 输出 NFT(B) | 指定 ID 购买 |
上述接口皆可跨交易池批量使用,入参都以数组形式传入,根据传入的交易信息顺序执行,如果遇到报错信息,则会交易回滚 (revert)。SudoSwap 还另外封装了一套不会 revert 的 robust 版本接口:
Method Name | robust version of |
---|---|
robustSwapETHForAnyNFTs | swapETHForAnyNFTs |
robustSwapETHForSpecificNFTs | swapETHForSpecificNFTs |
robustSwapERC20ForAnyNFTs | swapERC20ForAnyNFTs |
robustSwapERC20ForSpecificNFTs | swapERC20ForSpecificNFTs |
robustSwapNFTsForToken | swapNFTsForToken |
robustSwapETHForSpecificNFTsAndNFTsToToken | 输入 ETH 购买指定 ID 的 NFT 并将其卖成 ERC20 或 ETH |
robustSwapERC20ForSpecificNFTsAndNFTsToToken | 输入 ERC20 购买指定 ID 的 NFT 并将其卖成 ERC20 或 ETH |
下面将以两个典型的接口为例,讲解其代码逻辑。
swapETHForSpecificNFTs
输入 ETH 输出 NFT,NFT 将根据调用参数指定的 id 输出。入参:
- swapList: PairSwapSpecific 数组类型,每个元素都有 pair 地址 和 指定的 NFT ids
- ethRecipient: ETH 找零的接收地址,一般填写调用者地址
- nftRecipient: NFT 的接收地址,一般填写调用者地址
- deadline: 交易执行的最后期限,超过该期限交易将回滚作废;该机制与 Uniswap 相同,主要防止交易 pending 时间过长,而导致用户承担太大的滑点
struct PairSwapSpecific {
LSSVMPair pair;
uint256[] nftIds;
}
/**
@notice Swaps ETH into specific NFTs using multiple pairs.
@param swapList The list of pairs to trade with and the IDs of the NFTs to buy from each.
@param ethRecipient The address that will receive the unspent ETH input
@param nftRecipient The address that will receive the NFT output
@param deadline The Unix timestamp (in seconds) at/after which the swap will revert
@return remainingValue The unspent ETH amount
*/
function swapETHForSpecificNFTs(
PairSwapSpecific[] calldata swapList,
address payable ethRecipient,
address nftRecipient,
uint256 deadline
)
external
payable
checkDeadline(deadline)
returns (uint256 remainingValue)
{
return
_swapETHForSpecificNFTs(
swapList,
msg.value,
ethRecipient,
nftRecipient
);
}
_swapETHForSpecificNFTs
是内部执行批量交易的函数。入参:
- inputAmount: 即为
msg.value
用户传入购买 NFT 所使用的 ETH - swapList, ethRecipient, nftRecipient: 同上
函数逻辑:
- 遍历
swapList
, 顺序执行其中每一笔子交易- 调用对应的
LSSVpair
合约的getBuyNFTQuote
函数查询该子交易的精确开销 - 检查查询期间是否有 BoundingCurve 价格模型的报错,报错类型主要有购买数量不能为 0,价格数值不能溢出等;robust 版本的接口通常会忽略这里的报错,跳过执行下一条子交易;
- 实际执行子交易,转入刚才查询的精确开销数量,避免找零环节,这样做法可以有效节省 gas(省去了找零往回转账的 gas 费)
- remainingValue 扣除该子交易的实际开销
- 调用对应的
- 遍历子交易完成后,将剩余 remainingValue 返还给调用者
/**
@notice Internal function used to swap ETH for a specific set of NFTs
@param swapList The list of pairs and swap calldata
@param inputAmount The total amount of ETH to send
@param ethRecipient The address receiving excess ETH
@param nftRecipient The address receiving the NFTs from the pairs
@return remainingValue The unspent token amount
*/
function _swapETHForSpecificNFTs(
PairSwapSpecific[] calldata swapList,
uint256 inputAmount,
address payable ethRecipient,
address nftRecipient
) internal returns (uint256 remainingValue) {
remainingValue = inputAmount;
uint256 pairCost;
CurveErrorCodes.Error error;
// Do swaps
uint256 numSwaps = swapList.length;
for (uint256 i; i < numSwaps; ) {
// Calculate the cost per swap first to send exact amount of ETH over, saves gas by avoiding the need to send back excess ETH
(error, , , pairCost, ) = swapList[i].pair.getBuyNFTQuote(
swapList[i].nftIds.length
);
// Require no errors
require(error == CurveErrorCodes.Error.OK, "Bonding curve error");
// Total ETH taken from sender cannot exceed inputAmount
// because otherwise the deduction from remainingValue will fail
remainingValue -= swapList[i].pair.swapTokenForSpecificNFTs{
value: pairCost
}(
swapList[i].nftIds,
remainingValue,
nftRecipient,
true,
msg.sender
);
unchecked {
++i;
}
}
// Return remaining value to sender
if (remainingValue > 0) {
ethRecipient.safeTransferETH(remainingValue);
}
}
swapNFTsForToken
输入 NFT 输出 ETH/ERC20。入参:
- swapList: PairSwapSpecific 数组类型,每个元素都有 pair 地址 和 指定的 NFT ids
- minOutput: 规定最少的输出 ETH/ERC20 数量,若执行交易输出低于该值,则交易回滚;该机制主要为了防止滑点过大,让用户设置一个可以接受的阈值,通常为了防止三明治攻击;
- tokenRecipient: ERC20/ETH 接受地址
- deadline: 交易执行的最后期限,超过该期限交易将回滚作废;
/**
@notice Swaps NFTs into ETH/ERC20 using multiple pairs.
@param swapList The list of pairs to trade with and the IDs of the NFTs to sell to each.
@param minOutput The minimum acceptable total tokens received
@param tokenRecipient The address that will receive the token output
@param deadline The Unix timestamp (in seconds) at/after which the swap will revert
@return outputAmount The total tokens received
*/
function swapNFTsForToken(
PairSwapSpecific[] calldata swapList,
uint256 minOutput,
address tokenRecipient,
uint256 deadline
) external checkDeadline(deadline) returns (uint256 outputAmount) {
return _swapNFTsForToken(swapList, minOutput, payable(tokenRecipient));
}
_swapNFTsForToken
执行交易的内部函数。其函数逻辑如下:
- 遍历
swapList
, 顺序执行其中每一笔子交易- 调用该子交易的交易对的
swapNFTsForToken
函数,实际执行子交易;该函数会直接将 ERC20/ETH 转给接收者; - 累计
outputAmount
数值
- 调用该子交易的交易对的
- 遍历执行完毕,判断累计输出数量是否高于设置的
minOutput
, 否则交易 revert
/**
@notice Swaps NFTs for tokens, designed to be used for 1 token at a time
@dev Calling with multiple tokens is permitted, BUT minOutput will be
far from enough of a safety check because different tokens almost certainly have different unit prices.
@param swapList The list of pairs and swap calldata
@param minOutput The minimum number of tokens to be receieved frm the swaps
@param tokenRecipient The address that receives the tokens
@return outputAmount The number of tokens to be received
*/
function _swapNFTsForToken(
PairSwapSpecific[] calldata swapList,
uint256 minOutput,
address payable tokenRecipient
) internal returns (uint256 outputAmount) {
// Do swaps
uint256 numSwaps = swapList.length;
for (uint256 i; i < numSwaps; ) {
// Do the swap for token and then update outputAmount
// Note: minExpectedTokenOutput is set to 0 since we're doing an aggregate slippage check below
outputAmount += swapList[i].pair.swapNFTsForToken(
swapList[i].nftIds,
0,
tokenRecipient,
true,
msg.sender
);
unchecked {
++i;
}
}
// Aggregate slippage check
require(outputAmount >= minOutput, "outputAmount too low");
}
Factory
创建交易对合约的工厂合约。
LPs 使用 Factory 创建交易对 (LSSVMPair
合约) 时,需要选择做市类型:buy, sell, both (买入 NFT、卖出 NFT、同时买入卖出 NFT,根据注入资产类型和数量决定,稍后会详细说明);同时创建者也要选择该交易池所使用的 BoundingCurve 类型 (价格曲线), Linear 或者 Exponential.
createPairETH
创建 NFT 和 ETH 组成的交易对 LSSVPair 合约实例。入参:
_nft
: NFT 地址_bondingCurve
: 使用的定价模型合约地址,目前只有 Linear 和 Exponential 两种_assetRecipient
: 资产接受地址,通常是 0 地址,表示资产将由 Pair 合约接收,只有同时买入卖出的 Pair 可以设置非 0 地址;_poolType
: pool 类型_delta
: 定价模型的 delta_fee
: 交易手续费率_spotPrice
: 初始交易价格_initialNFTIDs
: 池子种初始 NFT 的 id
函数逻辑:
- 检查 boundingCurve 定价模型合约地址是否在规定的名单内
- EXPONENTIAL_CURVE: 0x432f962D8209781da23fB37b6B59ee15dE7d9841
- LINEAR_CURVE: 0x5B6aC51d9B1CeDE0068a1B26533CAce807f883Ee
- 根据 NFT 是否支持 enumerable 拓展标准,决定使用哪种创建模板
- LSSVMPairEnumerableETH
- LSSVMPairMissingEnumerableETH
- 调用模板合约 cloneETHPair 函数创建交易对 Pair
_initializePairETH()
初始化交易对合约
/**
@notice Creates a pair contract using EIP-1167.
@param _nft The NFT contract of the collection the pair trades
@param _bondingCurve The bonding curve for the pair to price NFTs, must be whitelisted
@param _assetRecipient The address that will receive the assets traders give during trades.
If set to address(0), assets will be sent to the pool address.
Not available to TRADE pools.
@param _poolType TOKEN, NFT, or TRADE
@param _delta The delta value used by the bonding curve. The meaning of delta depends
on the specific curve.
@param _fee The fee taken by the LP in each trade. Can only be non-zero if _poolType is Trade.
@param _spotPrice The initial selling spot price
@param _initialNFTIDs The list of IDs of NFTs to transfer from the sender to the pair
@return pair The new pair
*/
function createPairETH(
IERC721 _nft,
ICurve _bondingCurve,
address payable _assetRecipient,
LSSVMPair.PoolType _poolType,
uint128 _delta,
uint96 _fee,
uint128 _spotPrice,
uint256[] calldata _initialNFTIDs
) external payable returns (LSSVMPairETH pair) {
require(
bondingCurveAllowed[_bondingCurve],
"Bonding curve not whitelisted"
);
// Check to see if the NFT supports Enumerable to determine which template to use
address template;
try IERC165(address(_nft)).supportsInterface(INTERFACE_ID_ERC721_ENUMERABLE) returns (bool isEnumerable) {
template = isEnumerable ? address(enumerableETHTemplate)
: address(missingEnumerableETHTemplate);
} catch {
template = address(missingEnumerableETHTemplate);
}
pair = LSSVMPairETH(
payable(
template.cloneETHPair(
this,
_bondingCurve,
_nft,
uint8(_poolType)
)
)
);
_initializePairETH(
pair,
_nft,
_assetRecipient,
_delta,
_fee,
_spotPrice,
_initialNFTIDs
);
emit NewPair(address(pair));
}
_initializePairETH()
初始化交易对合约
- 调用 Pair 合约的 initialize 函数初始化
- 遍历
_initialNFTIDs
将 NFT 从用户转入 Pair 合约
function _initializePairETH(
LSSVMPairETH _pair,
IERC721 _nft,
address payable _assetRecipient,
uint128 _delta,
uint96 _fee,
uint128 _spotPrice,
uint256[] calldata _initialNFTIDs
) internal {
// initialize pair
_pair.initialize(msg.sender, _assetRecipient, _delta, _fee, _spotPrice);
// transfer initial ETH to pair
payable(address(_pair)).safeTransferETH(msg.value);
// transfer initial NFTs from sender to pair
uint256 numNFTs = _initialNFTIDs.length;
for (uint256 i; i < numNFTs; ) {
_nft.safeTransferFrom(
msg.sender,
address(_pair),
_initialNFTIDs[i]
);
unchecked {
++i;
}
}
}
Pair
交易对合约。LPs 可以创建多个 Pair 合约,每个 Pair 都可能有不同的 NFT 种类、数量,ETH 或者 ERC20,以及不同的定价模型。当交易者选中一种 NFT 进行交易时,由于单个 Pair 可能深度不足,或者交易者选择了特定的几个 NFT 分布在不同的 Pair 中,那么交易者需要调用 Router 合约传入批量交易的信息,由 Router 进行跨 Pair 的批量交易。
对于普通用户而言,并不需要记忆每一种 NFT 的所有 Pool 地址,sudoSwap 前端有一套链下 pool 匹配机制。由于其前端代码并未开源,不能得知匹配细节,根据前端使用体验来看,大致可以归纳 3 条规则:
- 买 NFT 优先选择价格低的 Pair,购买多个 NFT 会拆分成多个单个 NFT 的子交易,逐一进行比价
- 卖 NFT 优先选择价格高的 Pair,卖出多个 NFT 会拆分成多个单个 NFT 的子交易,逐一进行比价
- 交易价格会包含手续费,并且交易费一直由买方承担
LPs 添加流动性时可以选择三种类型的 Pair:
- TOKEN 单边添加 ETH 流动性,买 NFT
- NFT 单边添加 NFT 流动性,卖 NFT
- TRADE 同时添加 ETH 和 NFT 流动性,同时买卖
enum PoolType {
TOKEN,
NFT,
TRADE,
}
initialize
Pair 进行初始化的函数。函数逻辑:
- 检查 owner 不能为 0 地址,并设置 owner 地址
- 开启防重入攻击的机制
- 设置手续费率 fee,只有 TRADE 类型的 Pair 才能设置非 0 手续费率
- 设置 assetRecipient,只有 TRADE 类型的 Pair 才能设置为非 0 地址,其他情况则必须为 0 地址,表示默认由 Pair 合约接收资产
- 设置 delta 和 spotPrice
/**
@notice Called during pair creation to set initial parameters
@dev Only called once by factory to initialize.
We verify this by making sure that the current owner is address(0).
The Ownable library we use disallows setting the owner to be address(0), so this condition
should only be valid before the first initialize call.
@param _owner The owner of the pair
@param _assetRecipient The address that will receive the TOKEN or NFT sent to this pair during swaps. NOTE: If set to address(0), they will go to the pair itself.
@param _delta The initial delta of the bonding curve
@param _fee The initial % fee taken, if this is a trade pair
@param _spotPrice The initial price to sell an asset into the pair
*/
function initialize(
address _owner,
address payable _assetRecipient,
uint128 _delta,
uint96 _fee,
uint128 _spotPrice
) external payable {
require(owner() == address(0), "Initialized");
__Ownable_init(_owner);
__ReentrancyGuard_init();
ICurve _bondingCurve = bondingCurve();
PoolType _poolType = poolType();
if ((_poolType == PoolType.TOKEN) || (_poolType == PoolType.NFT)) {
require(_fee == 0, "Only Trade Pools can have nonzero fee");
assetRecipient = _assetRecipient;
} else if (_poolType == PoolType.TRADE) {
require(_fee < MAX_FEE, "Trade fee must be less than 90%");
require(
_assetRecipient == address(0),
"Trade pools can't set asset recipient"
);
fee = _fee;
}
require(_bondingCurve.validateDelta(_delta), "Invalid delta for curve");
require(
_bondingCurve.validateSpotPrice(_spotPrice),
"Invalid new spot price for curve"
);
delta = _delta;
spotPrice = _spotPrice;
}
getBuyNFTQuote
查询购买 numNFTs
个 NFT 的购买价格。函数逻辑主要调用 bondingCurve().getBuyInfo
来获取结果。
返回结果:
newSpotPrice
: 假如该交易购买完成后,池子的最新价格newDelta
: 假如该交易完成后,新的 Delta 值,一般 Delta 不会发生变化inputAmount
: 完成该交易预计需要的输入数量protocolFee
: 协议费率
/**
@dev Used as read function to query the bonding curve for buy pricing info
@param numNFTs The number of NFTs to buy from the pair
*/
function getBuyNFTQuote(uint256 numNFTs)
external
view
returns (
CurveErrorCodes.Error error,
uint256 newSpotPrice,
uint256 newDelta,
uint256 inputAmount,
uint256 protocolFee
)
{
(
error,
newSpotPrice,
newDelta,
inputAmount,
protocolFee
) = bondingCurve().getBuyInfo(
spotPrice,
delta,
numNFTs,
fee,
factory().protocolFeeMultiplier()
);
}
swapTokenForSpecificNFTs
指定 id 购买 NFT。入参:
nftIds
: 指定买入的 NFT idmaxExpectedTokenInput
: 规定用多少 token 买入,超过该阈值则交易 revertnftRecipient
: NFT 资产接收地址isRouter
: 是否为通过 Router 调用该方法,若为 true 则 token 从 Router 合约转入(用户交易时会先将 token 转入 Router 合约)routerCaller
: Router 地址
函数逻辑:
- 检查 Pair 类型,由于是买入 NFT,所以 poolType 只能是
NFT
或者TRADE
- 检查 nftIds 应至少存在一个 id
_calculateBuyInfoAndUpdatePoolParams
查询 协议费率 和 交易所需的输入 token- 向买家收取 token
- 向买家发送 NFT
- 向买家找零
/**
@notice Sends token to the pair in exchange for a specific set of NFTs
@dev To compute the amount of token to send, call bondingCurve.getBuyInfo
This swap is meant for users who want specific IDs. Also higher chance of
reverting if some of the specified IDs leave the pool before the swap goes through.
@param nftIds The list of IDs of the NFTs to purchase
@param maxExpectedTokenInput The maximum acceptable cost from the sender. If the actual
amount is greater than this value, the transaction will be reverted.
@param nftRecipient The recipient of the NFTs
@param isRouter True if calling from LSSVMRouter, false otherwise. Not used for
ETH pairs.
@param routerCaller If isRouter is true, ERC20 tokens will be transferred from this address. Not used for
ETH pairs.
@return inputAmount The amount of token used for purchase
*/
function swapTokenForSpecificNFTs(
uint256[] calldata nftIds,
uint256 maxExpectedTokenInput,
address nftRecipient,
bool isRouter,
address routerCaller
) external payable virtual nonReentrant returns (uint256 inputAmount) {
// Store locally to remove extra calls
ILSSVMPairFactoryLike _factory = factory();
ICurve _bondingCurve = bondingCurve();
// Input validation
{
PoolType _poolType = poolType();
require(
_poolType == PoolType.NFT || _poolType == PoolType.TRADE,
"Wrong Pool type"
);
require((nftIds.length > 0), "Must ask for > 0 NFTs");
}
// Call bonding curve for pricing information
uint256 protocolFee;
(protocolFee, inputAmount) = _calculateBuyInfoAndUpdatePoolParams(
nftIds.length,
maxExpectedTokenInput,
_bondingCurve,
_factory
);
_pullTokenInputAndPayProtocolFee(
inputAmount,
isRouter,
routerCaller,
_factory,
protocolFee
);
_sendSpecificNFTsToRecipient(nft(), nftRecipient, nftIds);
_refundTokenToSender(inputAmount);
emit SwapNFTOutPair();
}
_pullTokenInputAndPayProtocolFee
查询 协议费率 和 交易所需的输入 token。其内部主要逻辑是调用 _bondingCurve.getBuyInfo()
查询交易预计需要输入token 和 协议手续费
/**
@notice Calculates the amount needed to be sent into the pair for a buy and adjusts spot price or delta if necessary
@param numNFTs The amount of NFTs to purchase from the pair
@param maxExpectedTokenInput The maximum acceptable cost from the sender. If the actual
amount is greater than this value, the transaction will be reverted.
@param protocolFee The percentage of protocol fee to be taken, as a percentage
@return protocolFee The amount of tokens to send as protocol fee
@return inputAmount The amount of tokens total tokens receive
*/
function _calculateBuyInfoAndUpdatePoolParams(
uint256 numNFTs,
uint256 maxExpectedTokenInput,
ICurve _bondingCurve,
ILSSVMPairFactoryLike _factory
) internal returns (uint256 protocolFee, uint256 inputAmount) {
...
(
error,
newSpotPrice,
newDelta,
inputAmount,
protocolFee
) = _bondingCurve.getBuyInfo(
currentSpotPrice,
currentDelta,
numNFTs,
fee,
_factory.protocolFeeMultiplier()
);
...
}
关于 卖出 NFT 的函数逻辑,与买入类似,这里不再赘述。
BoundingCurve
BoundingCurve 是 sudoAMM 的定价模块,目前由两种定价模型,即 Linear 线性型 和 Exponential 指数型。LPs 创建 Pair 时需要指定使用哪种 BoundingCurve 定价模型,并设置 BoundingCurve 的初始参数,包括 StartPrice
初始价格、delta
价格变化量,如果是 TRADE
类型 Pair 则还需要设置 fee
交易费率。
Linear BoundingCurve
假设某个 Pair 使用 Linear BoundingCurve, 我们想要买入一个 NFT 那么定价的公式如下:
如果我们想要购买 n 个 NFT 则有:
根据等差数列求和公式则有:
LinearCurve.getBuyInfo
是线性定价模型计算交易所需的购买输入token,以及交易完成后最新的价格。其函数逻辑:
- 检查购买数量 > 0
- 检查购买后的价格不能数值溢出
newSpotPrice = spotPrice + delta * numItems
newSpotPrice <= type(uint128).max
- 根据上述公式计算购买所需的总输入资产价值
inputValue
- 注意
buySpotPrice = spotPrice + delta
- 注意
- 在
inputValue
基础上附加 交易手续费 和 协议手续费
// LinearCurve.sol
function getBuyInfo(
uint128 spotPrice,
uint128 delta,
uint256 numItems,
uint256 feeMultiplier,
uint256 protocolFeeMultiplier
)
external
pure
override
returns (
Error error,
uint128 newSpotPrice,
uint128 newDelta,
uint256 inputValue,
uint256 protocolFee
)
{
// We only calculate changes for buying 1 or more NFTs
if (numItems == 0) {
return (Error.INVALID_NUMITEMS, 0, 0, 0, 0);
}
// For a linear curve, the spot price increases by delta for each item bought
uint256 newSpotPrice_ = spotPrice + delta * numItems;
if (newSpotPrice_ > type(uint128).max) {
return (Error.SPOT_PRICE_OVERFLOW, 0, 0, 0, 0);
}
newSpotPrice = uint128(newSpotPrice_);
// Spot price is assumed to be the instant sell price. To avoid arbitraging LPs, we adjust the buy price upwards.
// If spot price for buy and sell were the same, then someone could buy 1 NFT and then sell for immediate profit.
// EX: Let S be spot price. Then buying 1 NFT costs S ETH, now new spot price is (S+delta).
// The same person could then sell for (S+delta) ETH, netting them delta ETH profit.
// If spot price for buy and sell differ by delta, then buying costs (S+delta) ETH.
// The new spot price would become (S+delta), so selling would also yield (S+delta) ETH.
uint256 buySpotPrice = spotPrice + delta;
// If we buy n items, then the total cost is equal to:
// (buy spot price) + (buy spot price + 1*delta) + (buy spot price + 2*delta) + ... + (buy spot price + (n-1)*delta)
// This is equal to n*(buy spot price) + (delta)*(n*(n-1))/2
// because we have n instances of buy spot price, and then we sum up from delta to (n-1)*delta
inputValue =
numItems *
buySpotPrice +
(numItems * (numItems - 1) * delta) /
2;
// Account for the protocol fee, a flat percentage of the buy amount
protocolFee = inputValue.fmul(
protocolFeeMultiplier,
FixedPointMathLib.WAD
);
// Account for the trade fee, only for Trade pools
inputValue += inputValue.fmul(feeMultiplier, FixedPointMathLib.WAD);
// Add the protocol fee to the required input amount
inputValue += protocolFee;
// Keep delta the same
newDelta = delta;
// If we got all the way here, no math error happened
error = Error.OK;
}
Exponential BoundingCurve
假设某个 Pair 使用 Exponential BoundingCurve, 我们想要买入一个 NFT 那么定价的公式如下:
注意此处 delta >= 1, 如果我们想要购买 n 个 NFT 则有:
根据等比数列求和公式则有:
ExponentialCurve.getBuyInfo
是指数定价模型计算交易所需的购买输入token,以及交易完成后最新的价格。其函数逻辑:
- 检查购买数量 > 0
- 检查购买后的价格
newSpotPrice
不能数值溢出deltaPowN = delta^numItems
newSpotPrice = spotPrice * deltaPowN
newSpotPrice <= type(uint128).max
- 根据上述公式计算购买所需的总输入资产价值
inputValue
- 注意
buySpotPrice = spotPrice * delta
- 注意
- 在
inputValue
基础上附加 交易手续费 和 协议手续费
// ExponentialCurve.sol
function getBuyInfo(
uint128 spotPrice,
uint128 delta,
uint256 numItems,
uint256 feeMultiplier,
uint256 protocolFeeMultiplier
)
external
pure
override
returns (
Error error,
uint128 newSpotPrice,
uint128 newDelta,
uint256 inputValue,
uint256 protocolFee
)
{
// NOTE: we assume delta is > 1, as checked by validateDelta()
// We only calculate changes for buying 1 or more NFTs
if (numItems == 0) {
return (Error.INVALID_NUMITEMS, 0, 0, 0, 0);
}
uint256 deltaPowN = uint256(delta).fpow(
numItems,
FixedPointMathLib.WAD
);
// For an exponential curve, the spot price is multiplied by delta for each item bought
uint256 newSpotPrice_ = uint256(spotPrice).fmul(
deltaPowN,
FixedPointMathLib.WAD
);
if (newSpotPrice_ > type(uint128).max) {
return (Error.SPOT_PRICE_OVERFLOW, 0, 0, 0, 0);
}
newSpotPrice = uint128(newSpotPrice_);
// Spot price is assumed to be the instant sell price. To avoid arbitraging LPs, we adjust the buy price upwards.
// If spot price for buy and sell were the same, then someone could buy 1 NFT and then sell for immediate profit.
// EX: Let S be spot price. Then buying 1 NFT costs S ETH, now new spot price is (S * delta).
// The same person could then sell for (S * delta) ETH, netting them delta ETH profit.
// If spot price for buy and sell differ by delta, then buying costs (S * delta) ETH.
// The new spot price would become (S * delta), so selling would also yield (S * delta) ETH.
uint256 buySpotPrice = uint256(spotPrice).fmul(
delta,
FixedPointMathLib.WAD
);
// If the user buys n items, then the total cost is equal to:
// buySpotPrice + (delta * buySpotPrice) + (delta^2 * buySpotPrice) + ... (delta^(numItems - 1) * buySpotPrice)
// This is equal to buySpotPrice * (delta^n - 1) / (delta - 1)
inputValue = buySpotPrice.fmul(
(deltaPowN - FixedPointMathLib.WAD).fdiv(
delta - FixedPointMathLib.WAD,
FixedPointMathLib.WAD
),
FixedPointMathLib.WAD
);
// Account for the protocol fee, a flat percentage of the buy amount
protocolFee = inputValue.fmul(
protocolFeeMultiplier,
FixedPointMathLib.WAD
);
// Account for the trade fee, only for Trade pools
inputValue += inputValue.fmul(feeMultiplier, FixedPointMathLib.WAD);
// Add the protocol fee to the required input amount
inputValue += protocolFee;
// Keep delta the same
newDelta = delta;
// If we got all the way here, no math error happened
error = Error.OK;
}
fees
我们注意到上述两个计算逻辑中,手续费都是单独附加给买方成本上。即当进行用户作为买方买入 NFT 时,会直接在买入 NFT 开销上直接按比例附加手续费和协议手续费。当买家所使用的 Pair 是 TRADE 类型时,交易手续费率 fee
大于0,其他两种类型 fee
都是0,而协议手续费率目前是固定的 0.5%。
而当用户作为卖方时,手续费和协议手续费依旧由用户承担。即作为 LP 方永远不承担手续费,而都是由对手方 (外部交易者)承担手续费。
举例说明,假设某 Pair 是 TRADE 类型,设置交易手续费率 20%:
- 用户想以 1 ETH 买入一个 NFT,则用户总共需要向 Pair 合约转入
1 ETH + 1 * 20% ETH + 1 * 0.5% ETH = 1.205 ETH
,注意第三项开销是协议手续费 - 用户想以 1 ETH 的价格向 Pair 出售一个 NFT,则用户将获得
1 ETH - 1 * 20% ETH - 1 * 0.5% ETH = 0.795 ETH