mirror of
https://github.com/luxfi/sudoamm.git
synced 2026-07-25 16:17:09 +00:00
add more tests to partial fill
This commit is contained in:
+75
-52
@@ -77,29 +77,33 @@ contract LSSVMRouter2 {
|
||||
}
|
||||
|
||||
// Given a pair and a number of items to buy, calculate the max price paid for 1 up to numNFTs to buy
|
||||
function getNFTQuoteForPartialFill(
|
||||
LSSVMPair pair,
|
||||
uint256 numNFTs,
|
||||
bool isBuy
|
||||
) external view returns (uint256[] memory) {
|
||||
function getNFTQuoteForPartialFillBuy(LSSVMPair pair, uint256 numNFTs)
|
||||
external
|
||||
view
|
||||
returns (uint256[] memory)
|
||||
{
|
||||
require(numNFTs > 0, "Nonzero");
|
||||
uint256[] memory prices = new uint256[](numNFTs);
|
||||
uint256 fullPrice;
|
||||
if (isBuy) {
|
||||
(, , , fullPrice, ) = pair.getBuyNFTQuote(numNFTs);
|
||||
} else {
|
||||
(, , , fullPrice, ) = pair.getSellNFTQuote(numNFTs);
|
||||
uint128 spotPrice = pair.spotPrice();
|
||||
uint128 delta = pair.delta();
|
||||
uint256 fee = pair.fee();
|
||||
for (uint256 i; i < numNFTs; i++) {
|
||||
uint256 price;
|
||||
(, spotPrice, delta, price, ) = pair.bondingCurve().getBuyInfo(
|
||||
spotPrice,
|
||||
delta,
|
||||
1,
|
||||
fee,
|
||||
pair.factory().protocolFeeMultiplier()
|
||||
);
|
||||
prices[i] = price;
|
||||
}
|
||||
prices[numNFTs - 1] = fullPrice;
|
||||
for (uint256 i = 0; i < numNFTs - 1; i++) {
|
||||
uint256 currentPrice;
|
||||
if (isBuy) {
|
||||
(, , , currentPrice, ) = pair.getBuyNFTQuote(numNFTs - i - 1);
|
||||
} else {
|
||||
(, , , currentPrice, ) = pair.getSellNFTQuote(numNFTs - i - 1);
|
||||
}
|
||||
prices[i] = fullPrice - currentPrice;
|
||||
uint256[] memory totalPrices = new uint256[](numNFTs);
|
||||
totalPrices[0] = prices[prices.length - 1];
|
||||
for (uint256 i = 1; i < numNFTs; i++) {
|
||||
totalPrices[i] = totalPrices[i - 1] + prices[prices.length - 1 - i];
|
||||
}
|
||||
return prices;
|
||||
return totalPrices;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,7 +113,7 @@ contract LSSVMRouter2 {
|
||||
function robustBuySellWithETHAndPartialFill(
|
||||
PairSwapSpecificPartialFill[] calldata buyList,
|
||||
PairSwapSpecificPartialFillForToken[] calldata sellList
|
||||
) external payable {
|
||||
) external payable returns (uint256 remainingValue) {
|
||||
// High level logic:
|
||||
// Go through each buy order
|
||||
// Check to see if the quote to buy all items is fillable given the max price
|
||||
@@ -122,10 +126,10 @@ contract LSSVMRouter2 {
|
||||
// Locally scope the buys
|
||||
{
|
||||
// Start with all of the ETH sent
|
||||
uint256 remainingValue = msg.value;
|
||||
remainingValue = msg.value;
|
||||
uint256 numBuys = buyList.length;
|
||||
|
||||
// Try each buy swaps
|
||||
// Try each buy swap
|
||||
for (uint256 i; i < numBuys; ) {
|
||||
LSSVMPair pair = buyList[i].swapInfo.pair;
|
||||
uint256 numNFTs = buyList[i].swapInfo.nftIds.length;
|
||||
@@ -165,6 +169,7 @@ contract LSSVMRouter2 {
|
||||
if (numItemsToFill == 0) {
|
||||
continue;
|
||||
} else {
|
||||
|
||||
// Figure out which items are actually still buyable from the list
|
||||
uint256[] memory fillableIds = _findAvailableIds(
|
||||
pair,
|
||||
@@ -172,12 +177,18 @@ contract LSSVMRouter2 {
|
||||
buyList[i].swapInfo.nftIds
|
||||
);
|
||||
|
||||
// If no IDs are fillable, then skip
|
||||
if (fillableIds.length == 0) {
|
||||
continue;
|
||||
// If we can actually only fill less items...
|
||||
if (fillableIds.length < numItemsToFill) {
|
||||
numItemsToFill = fillableIds.length;
|
||||
// If no IDs are fillable, then skip entirely
|
||||
if (numItemsToFill == 0) {
|
||||
continue;
|
||||
}
|
||||
// Otherwise, adjust the max amt sent to be down
|
||||
(,,,priceToFillAt,) = pair.getBuyNFTQuote(numItemsToFill);
|
||||
}
|
||||
|
||||
// Otherwise, do the partial fill swap with the updated price and ids
|
||||
// Now, do the partial fill swap with the updated price and ids
|
||||
remainingValue -= pair.swapTokenForSpecificNFTs{
|
||||
value: priceToFillAt
|
||||
}(
|
||||
@@ -263,10 +274,13 @@ contract LSSVMRouter2 {
|
||||
uint256 end = maxNumNFTs - 1;
|
||||
while (start <= end) {
|
||||
// Get price of mid number of items
|
||||
uint256 mid = start + (end - start + 1) / 2;
|
||||
(, , , uint256 currentPrice, ) = pair.getBuyNFTQuote(mid);
|
||||
uint256 mid = start + (end - start) / 2;
|
||||
|
||||
// mid is the index of the max price to buy mid+1 NFTs
|
||||
(, , , uint256 currentPrice, ) = pair.getBuyNFTQuote(mid + 1);
|
||||
|
||||
// If we pay at least the currentPrice with our maxPrice, record the value, and recurse on the right half
|
||||
if (currentPrice < maxPricesPerNumNFTs[mid]) {
|
||||
if (currentPrice <= maxPricesPerNumNFTs[mid]) {
|
||||
// We have to add 1 because mid is indexing into maxPricesPerNumNFTs which is 0-indexed
|
||||
numNFTs = mid + 1;
|
||||
price = currentPrice;
|
||||
@@ -274,6 +288,9 @@ contract LSSVMRouter2 {
|
||||
}
|
||||
// Otherwise, if it's beyond our budget, recurse on the left half (to find smth cheaper)
|
||||
else {
|
||||
if (mid == 0) {
|
||||
break;
|
||||
}
|
||||
end = mid - 1;
|
||||
}
|
||||
}
|
||||
@@ -289,28 +306,34 @@ contract LSSVMRouter2 {
|
||||
// Start and end indices
|
||||
uint256 start = 0;
|
||||
uint256 end = maxNumNFTs - 1;
|
||||
while (start <= end) {
|
||||
// Get price of mid number of items
|
||||
uint256 mid = start + (end - start + 1) / 2;
|
||||
(, , , uint256 currentPrice, ) = pair.getSellNFTQuote(mid);
|
||||
// If it costs more than there is ETH balance for, then recurse on the left half
|
||||
if (currentPrice > pairBalance) {
|
||||
end = mid - 1;
|
||||
}
|
||||
// Otherwise, we can proceed
|
||||
else {
|
||||
// If we can get at least minOutput selling this number of items, recurse on the right half
|
||||
if (currentPrice >= minOutputPerNumNFTs[mid]) {
|
||||
numNFTs = mid + 1;
|
||||
price = currentPrice;
|
||||
start = mid + 1;
|
||||
}
|
||||
// Otherwise, recurse on the left to find something better priced
|
||||
else {
|
||||
end = mid - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// while (start <= end) {
|
||||
// // Get price of mid number of items
|
||||
// uint256 mid = start + (end - start + 1) / 2;
|
||||
// (, , , uint256 currentPrice, ) = pair.getSellNFTQuote(mid + 1);
|
||||
// // If it costs more than there is ETH balance for, then recurse on the left half
|
||||
// if (currentPrice > pairBalance) {
|
||||
// if (mid == 1) {
|
||||
// break;
|
||||
// }
|
||||
// end = mid - 1;
|
||||
// }
|
||||
// // Otherwise, we can proceed
|
||||
// else {
|
||||
// // If we can get at least minOutput selling this number of items, recurse on the right half
|
||||
// if (currentPrice >= minOutputPerNumNFTs[mid]) {
|
||||
// numNFTs = mid + 1;
|
||||
// price = currentPrice;
|
||||
// start = mid + 1;
|
||||
// }
|
||||
// // Otherwise, recurse on the left to find something better priced
|
||||
// else {
|
||||
// if (mid == 1) {
|
||||
// break;
|
||||
// }
|
||||
// end = mid - 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Return numNFTs and price
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ pragma solidity ^0.8.0;
|
||||
import {DSTest} from "ds-test/test.sol";
|
||||
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
|
||||
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
||||
|
||||
import {ICurve} from "../../bonding-curves/ICurve.sol";
|
||||
import {LSSVMPairFactory} from "../../LSSVMPairFactory.sol";
|
||||
import {LSSVMPair} from "../../LSSVMPair.sol";
|
||||
@@ -19,6 +18,7 @@ import {LSSVMRouter} from "../../LSSVMRouter.sol";
|
||||
import {IERC721Mintable} from "../interfaces/IERC721Mintable.sol";
|
||||
import {Configurable} from "../mixins/Configurable.sol";
|
||||
import {RouterCaller} from "../mixins/RouterCaller.sol";
|
||||
import "../utils/console.sol";
|
||||
|
||||
/** Handles test cases where users try to buy multiple NFTs from a pool, but only get partially filled
|
||||
> $ forge test --match-contract RPF* -vvvvv
|
||||
@@ -126,14 +126,15 @@ abstract contract RouterPartialFill is
|
||||
*/
|
||||
|
||||
// The "base" case where no partial fill is needed, i.e. we buy all of the NFTs
|
||||
function test_swapTokenForSpecificNFTsFullFill() public {
|
||||
/*
|
||||
function test_defaultFullFill() public {
|
||||
// Run all cases from 1 to 10
|
||||
for (uint numNFTs = 1; numNFTs <= 10; numNFTs++) {
|
||||
for (uint256 numNFTs = 1; numNFTs <= 10; numNFTs++) {
|
||||
this.setUp();
|
||||
uint256 NUM_NFTS = numNFTs;
|
||||
uint256 startNFTBalance = test721.balanceOf(address(this));
|
||||
|
||||
// Only 1 entry
|
||||
// Only 1 pool we're buying from
|
||||
LSSVMRouter2.PairSwapSpecificPartialFill[]
|
||||
memory buyList = new LSSVMRouter2.PairSwapSpecificPartialFill[](
|
||||
1
|
||||
@@ -147,33 +148,37 @@ abstract contract RouterPartialFill is
|
||||
|
||||
// Get partial fill prices
|
||||
uint256[] memory partialFillPrices = router
|
||||
.getNFTQuoteForPartialFill(pair, NUM_NFTS, true);
|
||||
.getNFTQuoteForPartialFillBuy(pair, NUM_NFTS);
|
||||
|
||||
// Create the partial fill args
|
||||
LSSVMRouter2.PairSwapSpecific memory swapInfo = LSSVMRouter2
|
||||
.PairSwapSpecific({pair: pair, nftIds: ids});
|
||||
|
||||
buyList[0] = LSSVMRouter2.PairSwapSpecificPartialFill({
|
||||
swapInfo: swapInfo,
|
||||
swapInfo: LSSVMRouter2.PairSwapSpecific({
|
||||
pair: pair,
|
||||
nftIds: ids
|
||||
}),
|
||||
expectedSpotPrice: SPOT_PRICE,
|
||||
maxCostPerNumNFTs: partialFillPrices
|
||||
});
|
||||
|
||||
// Create empty sell list
|
||||
LSSVMRouter2.PairSwapSpecificPartialFillForToken[] memory emptySellList = new LSSVMRouter2.PairSwapSpecificPartialFillForToken[](0);
|
||||
LSSVMRouter2.PairSwapSpecificPartialFillForToken[]
|
||||
memory emptySellList = new LSSVMRouter2.PairSwapSpecificPartialFillForToken[](
|
||||
0
|
||||
);
|
||||
string memory UNIMPLEMENTED = "Unimplemented";
|
||||
|
||||
// See if last value of maxCost is the same as getBuyNFTQuote(NUM_NFTS)
|
||||
// See if last value of maxCost is the same as getBuyNFTQuote(NUM_NFTS) (they should be equal)
|
||||
(, , , uint256 correctQuote, ) = pair.getBuyNFTQuote(NUM_NFTS);
|
||||
require(
|
||||
correctQuote == partialFillPrices[NUM_NFTS - 1],
|
||||
"Incorrect quote"
|
||||
);
|
||||
|
||||
// Do the actual partial fill
|
||||
try
|
||||
this.buyAndSellWithPartialFill{
|
||||
value: partialFillPrices[NUM_NFTS - 1]
|
||||
}(router, buyList, emptySellList)
|
||||
}(router, buyList, emptySellList)
|
||||
{
|
||||
uint256 endNFTBalance = test721.balanceOf(address(this));
|
||||
require(
|
||||
@@ -187,6 +192,123 @@ abstract contract RouterPartialFill is
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
- Two issues
|
||||
- getNFTQuoteForPartialFill seems to be incorrect for exponential pools, which is really weird
|
||||
-
|
||||
*/
|
||||
|
||||
// We buy 1-9 items first, then attempt to partial fill the rest
|
||||
// This is a case where:
|
||||
// - Not all items are there
|
||||
// - Not all items are in price range
|
||||
function test_restrictedPartialFill() public {
|
||||
// First buy 1-9 items (asc), then attempt to partial fill 9-1 items (desc)
|
||||
for (
|
||||
uint256 numNFTsToBuyFirst = 1;
|
||||
numNFTsToBuyFirst <= 9;
|
||||
numNFTsToBuyFirst++
|
||||
) {
|
||||
this.setUp();
|
||||
|
||||
// Randomize delta
|
||||
// uint128 modifiedDelta = this.modifyDelta(delta);
|
||||
// pair.changeDelta(modifiedDelta);
|
||||
|
||||
// Construct partial fill args first (below we fill some items before doing partial fill)
|
||||
LSSVMRouter2.PairSwapSpecificPartialFill[]
|
||||
memory buyList = new LSSVMRouter2.PairSwapSpecificPartialFill[](
|
||||
1
|
||||
);
|
||||
uint256[] memory ids = new uint256[](10);
|
||||
// Get all IDs
|
||||
for (uint256 i = 1; i <= 10; i++) {
|
||||
ids[i - 1] = 10 + i;
|
||||
}
|
||||
// Get partial fill prices
|
||||
uint256[] memory partialFillPrices = router
|
||||
.getNFTQuoteForPartialFillBuy(pair, 10);
|
||||
// Create the partial fill args
|
||||
buyList[0] = LSSVMRouter2.PairSwapSpecificPartialFill({
|
||||
swapInfo: LSSVMRouter2.PairSwapSpecific({
|
||||
pair: pair,
|
||||
nftIds: ids
|
||||
}),
|
||||
expectedSpotPrice: SPOT_PRICE,
|
||||
maxCostPerNumNFTs: partialFillPrices
|
||||
});
|
||||
// Create empty sell list
|
||||
LSSVMRouter2.PairSwapSpecificPartialFillForToken[]
|
||||
memory emptySellList = new LSSVMRouter2.PairSwapSpecificPartialFillForToken[](
|
||||
0
|
||||
);
|
||||
string memory UNIMPLEMENTED = "Unimplemented";
|
||||
|
||||
// ** Doing the preeempetive buy **
|
||||
// Set IDs to preemptively buy
|
||||
uint256[] memory nftIdsToBuyFirst = new uint256[](
|
||||
numNFTsToBuyFirst
|
||||
);
|
||||
for (uint256 i = 1; i <= numNFTsToBuyFirst; i++) {
|
||||
nftIdsToBuyFirst[i - 1] = 10 + i;
|
||||
}
|
||||
(, , , uint256 initialQuote, ) = pair.getBuyNFTQuote(
|
||||
numNFTsToBuyFirst
|
||||
);
|
||||
LSSVMRouter2.RobustPairSwapSpecific[]
|
||||
memory initialBuyList = new LSSVMRouter2.RobustPairSwapSpecific[](
|
||||
1
|
||||
);
|
||||
initialBuyList[0] = LSSVMRouter2.RobustPairSwapSpecific({
|
||||
swapInfo: LSSVMRouter2.PairSwapSpecific({
|
||||
pair: pair,
|
||||
nftIds: nftIdsToBuyFirst
|
||||
}),
|
||||
maxCost: initialQuote
|
||||
});
|
||||
// Buy these items first (if we can)
|
||||
pair.swapTokenForSpecificNFTs{
|
||||
value: this.modifyInputAmount(initialQuote)
|
||||
}(
|
||||
nftIdsToBuyFirst,
|
||||
initialQuote,
|
||||
address(this),
|
||||
false,
|
||||
address(this)
|
||||
);
|
||||
|
||||
// Get NFT balance now (after the partial fill)
|
||||
uint256 startNFTBalance = test721.balanceOf(address(this));
|
||||
|
||||
// Do the actual partial fill
|
||||
try
|
||||
this.buyAndSellWithPartialFill{value: partialFillPrices[9]}( // We always pass in the maximal amount of ETH possible, we should get a refund
|
||||
router,
|
||||
buyList,
|
||||
emptySellList
|
||||
)
|
||||
returns (uint256 remainingValue) {
|
||||
uint256 endNFTBalance = test721.balanceOf(address(this));
|
||||
uint256 numNFTsAcquired = endNFTBalance - startNFTBalance;
|
||||
if (numNFTsAcquired > 0) {
|
||||
uint256 amountPaid = partialFillPrices[9] - remainingValue;
|
||||
uint256 maxBudget = partialFillPrices[numNFTsAcquired - 1];
|
||||
if (amountPaid > maxBudget) {
|
||||
console.log(numNFTsAcquired);
|
||||
console.log(amountPaid);
|
||||
console.log(maxBudget);
|
||||
}
|
||||
require(amountPaid <= maxBudget, "Overpaid");
|
||||
}
|
||||
} catch Error(string memory reason) {
|
||||
if (this.compareStrings(reason, UNIMPLEMENTED)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All the other cases
|
||||
}
|
||||
|
||||
@@ -70,5 +70,10 @@ abstract contract RouterCaller {
|
||||
LSSVMRouter2 router,
|
||||
LSSVMRouter2.PairSwapSpecificPartialFill[] calldata buyList,
|
||||
LSSVMRouter2.PairSwapSpecificPartialFillForToken[] calldata sellList
|
||||
) public payable virtual;
|
||||
) public payable virtual returns (uint256);
|
||||
|
||||
function swapETHForSpecificNFTs(
|
||||
LSSVMRouter2 router,
|
||||
LSSVMRouter2.RobustPairSwapSpecific[] calldata buyList
|
||||
) public payable virtual returns (uint256);
|
||||
}
|
||||
|
||||
@@ -210,7 +210,14 @@ abstract contract UsingERC20 is Configurable, RouterCaller {
|
||||
LSSVMRouter2 router,
|
||||
LSSVMRouter2.PairSwapSpecificPartialFill[] calldata buyList,
|
||||
LSSVMRouter2.PairSwapSpecificPartialFillForToken[] calldata sellList
|
||||
) public payable override {
|
||||
require(false, "Unimplemented");
|
||||
) public payable override returns (uint256) {
|
||||
require(false, "Unimplemented");
|
||||
}
|
||||
|
||||
function swapETHForSpecificNFTs(
|
||||
LSSVMRouter2 router,
|
||||
LSSVMRouter2.RobustPairSwapSpecific[] calldata buyList
|
||||
) public payable override returns (uint256) {
|
||||
require(false, "Unimplemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,9 +183,16 @@ abstract contract UsingETH is Configurable, RouterCaller {
|
||||
LSSVMRouter2 router,
|
||||
LSSVMRouter2.PairSwapSpecificPartialFill[] calldata buyList,
|
||||
LSSVMRouter2.PairSwapSpecificPartialFillForToken[] calldata sellList
|
||||
) public payable override {
|
||||
) public payable override returns (uint256) {
|
||||
return router.robustBuySellWithETHAndPartialFill{value: msg.value}(
|
||||
buyList, sellList
|
||||
);
|
||||
}
|
||||
|
||||
function swapETHForSpecificNFTs(
|
||||
LSSVMRouter2 router,
|
||||
LSSVMRouter2.RobustPairSwapSpecific[] calldata buyList
|
||||
) public payable override returns (uint256) {
|
||||
return router.swapETHForSpecificNFTs{value: msg.value}(buyList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,6 @@ abstract contract UsingExponentialCurve is Configurable {
|
||||
|
||||
// Return 1 eth as spot price and 10% as the delta scaling
|
||||
function getParamsForPartialFillTest() public pure override returns (uint128 spotPrice, uint128 delta) {
|
||||
return (10**18, 11**18);
|
||||
return (10**18, 1.1*(10**18));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user