add more tests to partial fill

This commit is contained in:
Owen
2022-08-28 14:18:55 -07:00
parent f82834a9de
commit 494b308f5d
7 changed files with 1765 additions and 69 deletions
+75 -52
View File
@@ -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
}
+134 -12
View File
@@ -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
}
+6 -1
View File
@@ -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);
}
+9 -2
View File
@@ -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");
}
}
+8 -1
View File
@@ -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);
}
}
+1 -1
View File
@@ -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