Liquidity Party adapter

This commit is contained in:
tim
2025-12-10 15:53:45 -04:00
parent f0f2f20c3c
commit f1e602d414
21 changed files with 1589 additions and 321 deletions

View File

@@ -12,7 +12,6 @@ contract AdapterTest is Test, ISwapAdapterTypes {
using FractionMath for Fraction;
using EfficientERC20 for IERC20;
uint256 constant pricePrecision = 10e24;
string[] public stringPctgs = ["0%", "0.1%", "50%", "100%"];
// @notice Test the behavior of a swap adapter for a list of pools
@@ -44,9 +43,10 @@ contract AdapterTest is Test, ISwapAdapterTypes {
}
// Prices should:
// 1. Be monotonic decreasing
// 1. Be monotonic decreasing (within rounding tolerance)
// 2. Be positive
// 3. Always be >= the executed price and >= the price after the swap
// (within rounding tolerance)
function testPricesForPair(
ISwapAdapter adapter,
bytes32 poolId,
@@ -76,7 +76,10 @@ contract AdapterTest is Test, ISwapAdapterTypes {
Fraction[] memory prices =
adapter.price(poolId, tokenIn, tokenOut, amounts);
assertGt(
fractionToInt(prices[0]),
fractionToInt(prices[0])
// within rounding tolerance
* (amounts[amounts.length - 1] + 1)
/ amounts[amounts.length - 1],
fractionToInt(prices[prices.length - 1]),
"Price at limit should be smaller than price at 0"
);
@@ -92,7 +95,6 @@ contract AdapterTest is Test, ISwapAdapterTypes {
uint256 priceAtZero = fractionToInt(prices[0]);
console2.log("TEST: Price at 0: %d", priceAtZero);
Trade memory trade;
deal(tokenIn, address(this), 5 * amounts[amounts.length - 1]);
uint256 initialState = vm.snapshot();
@@ -104,50 +106,93 @@ contract AdapterTest is Test, ISwapAdapterTypes {
amounts[j]
);
uint256 priceAtAmount = fractionToInt(prices[j]);
// We allow the assertions to tolerate rounding errors
// not greater than `1/amounts[j]`
uint256 toleranceDenominator = amounts[j];
console2.log("TEST: Swapping %d of %s", amounts[j], tokenIn);
trade = adapter.swap(
try adapter.swap(
poolId, tokenIn, tokenOut, OrderSide.Sell, amounts[j]
);
uint256 executedPrice =
trade.calculatedAmount * pricePrecision / amounts[j];
uint256 priceAfterSwap = fractionToInt(trade.price);
console2.log("TEST: - Executed price: %d", executedPrice);
console2.log("TEST: - Price at amount: %d", priceAtAmount);
console2.log("TEST: - Price after swap: %d", priceAfterSwap);
) returns (
Trade memory trade
) {
uint256 executedPrice = Fraction(
trade.calculatedAmount, amounts[j]
).toQ128x128();
uint256 priceAfterSwap = fractionToInt(trade.price);
console2.log("TEST: - Executed price: %d", executedPrice);
console2.log("TEST: - Price at amount: %d", priceAtAmount);
console2.log("TEST: - Price after swap: %d", priceAfterSwap);
if (hasPriceImpact) {
assertGe(
executedPrice,
priceAtAmount,
"Price should be greated than executed price."
);
assertGt(
executedPrice,
priceAfterSwap,
"Executed price should be greater than price after swap."
);
assertGt(
priceAtZero,
executedPrice,
"Price should be greated than price after swap."
);
} else {
assertGe(
priceAtZero,
priceAfterSwap,
"Executed price should be or equal to price after swap."
);
assertGe(
priceAtZero,
priceAtAmount,
"Executed price should be or equal to price after swap."
);
assertGe(
priceAtZero,
executedPrice,
"Price should be or equal to price after swap."
);
if (hasPriceImpact) {
assertGeTol(
executedPrice,
priceAtAmount,
toleranceDenominator,
"Price should be greater than executed price."
);
assertGtTol(
executedPrice,
priceAfterSwap,
toleranceDenominator,
"Executed price should be greater than price after swap."
);
assertGtTol(
priceAtZero,
executedPrice,
toleranceDenominator,
"Price should be greater than price after swap."
);
} else {
assertGeTol(
priceAtZero,
priceAfterSwap,
toleranceDenominator,
"Executed price should be or equal to price after swap."
);
assertGeTol(
priceAtZero,
priceAtAmount,
toleranceDenominator,
"Executed price should be or equal to price after swap."
);
assertGeTol(
priceAtZero,
executedPrice,
toleranceDenominator,
"Price should be or equal to price after swap."
);
}
} catch (bytes memory reason) {
(bool isTooSmall, uint256 lowerLimit) =
decodeTooSmallError(reason);
(bool isLimitExceeded, uint256 limit) =
decodeLimitExceededError(reason);
if (isTooSmall) {
// We allow a TooSmall exception to occur for the smallest
// amount only.
if (j == 1) {
console2.log(
"TEST: TooSmall exception tolerated for smallest amount"
);
} else {
revert(
"TEST: TooSmall thrown for a significantly sized amount"
);
}
} else if (isLimitExceeded) {
// We never allow LimitExceeded to be thrown, since all
// amounts should be within the stated limits.
revert(
"TEST: LimitExceeded thrown for an amount within limits"
);
} else {
// any other revert reason bubbles up
assembly {
revert(add(reason, 32), mload(reason))
}
}
}
vm.revertTo(initialState);
@@ -185,24 +230,87 @@ contract AdapterTest is Test, ISwapAdapterTypes {
);
uint256[] memory aboveLimitArray = new uint256[](1);
aboveLimitArray[0] = amountAboveLimit;
bool supportsLimitExceeded = false;
try adapter.price(poolId, tokenIn, tokenOut, aboveLimitArray) {
revert(
"Pool shouldn't be able to fetch prices above the sell limit"
);
} catch Error(string memory s) {
console2.log(
"TEST: Expected error when fetching price above limit: %s", s
);
} catch (bytes memory reason) {
(bool isTooSmall, uint256 lowerLimit) = decodeTooSmallError(reason);
(bool isLimitExceeded, uint256 limit) =
decodeLimitExceededError(reason);
if (isLimitExceeded) {
supportsLimitExceeded = true;
console2.log(
"TEST: LimitExceeded supported! Thrown when fetching price above limit: %i",
limit
);
} else if (isTooSmall) {
console2.log(
"TEST: UNEXPECTED TooSmall error when fetching price below limit: %i",
lowerLimit
);
revert TooSmall(lowerLimit);
} else if (
reason.length >= 4
&& bytes4(reason) == bytes4(keccak256("Error(string)"))
) {
string memory s = abi.decode(
sliceBytes(reason, 4, reason.length - 4), (string)
);
console2.log(
"TEST: Expected error when fetching price above limit: %s",
s
);
} else {
// Unexpected error type: re-raise.
assembly {
revert(add(reason, 32), mload(reason))
}
}
}
try adapter.swap(
poolId, tokenIn, tokenOut, OrderSide.Sell, aboveLimitArray[0]
) {
revert("Pool shouldn't be able to swap above the sell limit");
} catch Error(string memory s) {
console2.log(
"TEST: Expected error when swapping above limit: %s", s
);
} catch (bytes memory reason) {
(bool isTooSmall, uint256 lowerLimit) = decodeTooSmallError(reason);
(bool isLimitExceeded, uint256 limit) =
decodeLimitExceededError(reason);
if (isLimitExceeded) {
supportsLimitExceeded = true;
console2.log(
"TEST: LimitExceeded supported! Thrown when swapping above limit: %i",
limit
);
} else if (isTooSmall) {
console2.log(
"TEST: UNEXPECTED TooSmall error when swapping above limit: %i",
lowerLimit
);
revert TooSmall(lowerLimit);
} else if (
reason.length >= 4
&& bytes4(reason) == bytes4(keccak256("Error(string)"))
) {
string memory s = abi.decode(
sliceBytes(reason, 4, reason.length - 4), (string)
);
console2.log(
"TEST: Expected error when swapping above limit: %s", s
);
} else {
// Unexpected error type: re-raise.
assembly {
revert(add(reason, 32), mload(reason))
}
}
}
if (supportsLimitExceeded) {
console.log(unicode"Adapter supports LimitExceeded ✓");
}
}
@@ -244,7 +352,7 @@ contract AdapterTest is Test, ISwapAdapterTypes {
pure
returns (uint256)
{
return price.numerator * pricePrecision / price.denominator;
return price.toQ128x128();
}
function hasCapability(
@@ -259,4 +367,85 @@ contract AdapterTest is Test, ISwapAdapterTypes {
return false;
}
//
// Custom Error Helper Functions
// TODO should we expose these in a better location / library for solvers to
// also leverage?
// Helper function to check if error is TooSmall and decode it
function decodeTooSmallError(bytes memory reason)
internal
pure
returns (bool, uint256)
{
if (reason.length >= 4 && bytes4(reason) == TooSmall.selector) {
if (reason.length == 36) {
uint256 lowerLimit =
abi.decode(sliceBytes(reason, 4, 32), (uint256));
return (true, lowerLimit);
}
}
return (false, 0);
}
// Helper function to check if error is LimitExceeded and decode it
function decodeLimitExceededError(bytes memory reason)
internal
pure
returns (bool, uint256)
{
if (reason.length >= 4 && bytes4(reason) == LimitExceeded.selector) {
if (reason.length == 36) {
uint256 limit = abi.decode(sliceBytes(reason, 4, 32), (uint256));
return (true, limit);
}
}
return (false, 0);
}
// Helper function to slice bytes
function sliceBytes(bytes memory data, uint256 start, uint256 length)
internal
pure
returns (bytes memory)
{
bytes memory result = new bytes(length);
for (uint256 i = 0; i < length; i++) {
result[i] = data[start + i];
}
return result;
}
//
// Helper functions to assert with tolerance
//
function assertGeTol(
uint256 a,
uint256 b,
uint256 toleranceDenominator,
string memory errorMessage
) internal {
// The tolerance is `1 / toleranceDenominator`, so we increase the value
// of `a` by this amount. adjustedA = a * (denom+1) / denom
uint256 adjustedA = FractionMath.mulDiv(
a, toleranceDenominator + 1, toleranceDenominator
);
assertGe(adjustedA, b, errorMessage);
}
function assertGtTol(
uint256 a,
uint256 b,
uint256 toleranceDenominator,
string memory errorMessage
) internal {
// The tolerance is `1 / toleranceDenominator`, so we increase the value
// of `a` by this amount. adjustedA = a * (denom+1) / denom
uint256 adjustedA = FractionMath.mulDiv(
a, toleranceDenominator + 1, toleranceDenominator
);
assertGt(adjustedA, b, errorMessage);
}
}