Implement adapter integration test and use it on uniswap and balancer
This commit is contained in:
@@ -188,10 +188,11 @@ contract BalancerV2SwapAdapter is ISwapAdapter {
|
||||
override
|
||||
returns (Capability[] memory capabilities)
|
||||
{
|
||||
capabilities = new Capability[](3);
|
||||
capabilities = new Capability[](4);
|
||||
capabilities[0] = Capability.SellOrder;
|
||||
capabilities[1] = Capability.BuyOrder;
|
||||
capabilities[2] = Capability.PriceFunction;
|
||||
capabilities[3] = Capability.HardLimits;
|
||||
}
|
||||
|
||||
function getTokens(bytes32 poolId)
|
||||
|
||||
@@ -30,7 +30,10 @@ interface ISwapAdapterTypes {
|
||||
TokenBalanceIndependent,
|
||||
// Indicates that prices are returned scaled, else it is assumed prices
|
||||
// still require scaling by token decimals. (required)
|
||||
ScaledPrices
|
||||
ScaledPrices,
|
||||
// Indicates that if we try to go over the sell limits, the pool will
|
||||
// revert (optional)
|
||||
HardLimits
|
||||
}
|
||||
|
||||
/// @dev Representation used for rational numbers such as prices.
|
||||
|
||||
235
evm/test/AdapterTest.sol
Normal file
235
evm/test/AdapterTest.sol
Normal file
@@ -0,0 +1,235 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
||||
import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
|
||||
import "src/interfaces/ISwapAdapterTypes.sol";
|
||||
import "src/libraries/FractionMath.sol";
|
||||
|
||||
contract AdapterTest is Test, ISwapAdapterTypes {
|
||||
using FractionMath for Fraction;
|
||||
|
||||
uint256 constant pricePrecision = 10e24;
|
||||
string[] public stringPctgs = ["0%", "0.01%", "50%", "100%"];
|
||||
|
||||
// @notice Test the behavior of a swap adapter for a list of pools
|
||||
// @dev Computes limits, prices, and swaps on the pools on both directions
|
||||
// for different
|
||||
// sell amounts. Asserts that the prices behaves as expected.
|
||||
// @param adapter The swap adapter to test
|
||||
// @param poolIds The list of pool ids to test
|
||||
function testPoolBehaviour(ISwapAdapter adapter, bytes32[] memory poolIds)
|
||||
public
|
||||
{
|
||||
bool hasPriceImpact = !hasCapability(
|
||||
adapter.getCapabilities(poolIds[0], address(0), address(0)),
|
||||
Capability.ConstantPrice
|
||||
);
|
||||
for (uint256 i = 0; i < poolIds.length; i++) {
|
||||
address[] memory tokens = adapter.getTokens(poolIds[i]);
|
||||
IERC20(tokens[0]).approve(address(adapter), type(uint256).max);
|
||||
|
||||
testPricesForPair(
|
||||
adapter, poolIds[i], tokens[0], tokens[1], hasPriceImpact
|
||||
);
|
||||
testPricesForPair(
|
||||
adapter, poolIds[i], tokens[1], tokens[0], hasPriceImpact
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Prices should:
|
||||
// 1. Be monotonic decreasing
|
||||
// 2. Be positive
|
||||
// 3. Always be >= the executed price and >= the price after the swap
|
||||
function testPricesForPair(
|
||||
ISwapAdapter adapter,
|
||||
bytes32 poolId,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
bool hasPriceImpact
|
||||
) internal {
|
||||
uint256 sellLimit = adapter.getLimits(poolId, tokenIn, tokenOut)[0];
|
||||
assertGt(sellLimit, 0, "Sell limit should be greater than 0");
|
||||
|
||||
console2.log(
|
||||
"TEST: Testing prices for pair %s -> %s. Sell limit: %d",
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
sellLimit
|
||||
);
|
||||
uint256[] memory amounts = calculateTestAmounts(sellLimit);
|
||||
Fraction[] memory prices =
|
||||
adapter.price(poolId, tokenIn, tokenOut, amounts);
|
||||
assertGt(
|
||||
fractionToInt(prices[0]),
|
||||
fractionToInt(prices[prices.length - 1]),
|
||||
"Price at limit should be smaller than price at 0"
|
||||
);
|
||||
console2.log(
|
||||
"TEST: Price at 0: %d, price at sell limit: %d",
|
||||
fractionToInt(prices[0]),
|
||||
fractionToInt(prices[prices.length - 1])
|
||||
);
|
||||
|
||||
console2.log("TEST: Testing behavior for price at 0");
|
||||
assertGt(prices[0].numerator, 0, "Nominator shouldn't be 0");
|
||||
assertGt(prices[0].denominator, 0, "Denominator shouldn't be 0");
|
||||
|
||||
Trade memory trade;
|
||||
deal(tokenIn, address(this), 2 * amounts[amounts.length - 1]);
|
||||
|
||||
for (uint256 j = 1; j < amounts.length; j++) {
|
||||
console2.log(
|
||||
"TEST: Testing behavior for price at %s of limit: %d",
|
||||
stringPctgs[j],
|
||||
amounts[j]
|
||||
);
|
||||
uint256 priceAtAmount = fractionToInt(prices[j]);
|
||||
assertGt(prices[j].numerator, 0, "Nominator shouldn't be 0");
|
||||
assertGt(prices[j].denominator, 0, "Denominator shouldn't be 0");
|
||||
|
||||
console2.log("TEST: Swapping %d of %s", amounts[j], tokenIn);
|
||||
trade = adapter.swap(
|
||||
poolId, tokenIn, tokenOut, OrderSide.Sell, amounts[j]
|
||||
);
|
||||
uint256 executedPrice =
|
||||
trade.calculatedAmount * pricePrecision / amounts[j];
|
||||
uint256 priceAfterSwap = fractionToInt(trade.price);
|
||||
console2.log("TEST: Pool price: %d", priceAtAmount);
|
||||
console2.log("TEST: Executed price: %d", executedPrice);
|
||||
console2.log("TEST: Price after swap: %d", priceAfterSwap);
|
||||
|
||||
if (hasPriceImpact) {
|
||||
assertGt(
|
||||
priceAtAmount,
|
||||
executedPrice,
|
||||
"Price should be greated than executed price."
|
||||
);
|
||||
assertGt(
|
||||
executedPrice,
|
||||
priceAfterSwap,
|
||||
"Executed price should be greater than price after swap."
|
||||
);
|
||||
assertGt(
|
||||
priceAtAmount,
|
||||
priceAfterSwap,
|
||||
"Price should be greated than price after swap."
|
||||
);
|
||||
} else {
|
||||
assertGe(
|
||||
priceAtAmount,
|
||||
executedPrice,
|
||||
"Price should be greated than executed price."
|
||||
);
|
||||
assertGe(
|
||||
executedPrice,
|
||||
priceAfterSwap,
|
||||
"Executed price should be greater than price after swap."
|
||||
);
|
||||
assertGe(
|
||||
priceAtAmount,
|
||||
priceAfterSwap,
|
||||
"Price should be greated than price after swap."
|
||||
);
|
||||
}
|
||||
}
|
||||
uint256 amountAboveLimit = sellLimit * 105 / 100;
|
||||
|
||||
bool hasHardLimits = hasCapability(
|
||||
adapter.getCapabilities(poolId, tokenIn, tokenOut),
|
||||
Capability.HardLimits
|
||||
);
|
||||
|
||||
if (hasHardLimits) {
|
||||
testRevertAboveLimit(
|
||||
adapter, poolId, tokenIn, tokenOut, amountAboveLimit
|
||||
);
|
||||
} else {
|
||||
testOperationsAboveLimit(
|
||||
adapter, poolId, tokenIn, tokenOut, amountAboveLimit
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function testRevertAboveLimit(
|
||||
ISwapAdapter adapter,
|
||||
bytes32 poolId,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 amountAboveLimit
|
||||
) internal {
|
||||
console2.log("TEST: Testing revert behavior above the sell limit");
|
||||
uint256[] memory aboveLimitArray = new uint256[](1);
|
||||
aboveLimitArray[0] = amountAboveLimit;
|
||||
|
||||
try adapter.price(poolId, tokenIn, tokenOut, aboveLimitArray) {
|
||||
revert("Pool shouldn't be fetch prices above the sell limit");
|
||||
} catch Error(string memory s) {
|
||||
console2.log(
|
||||
"TEST: Expected error when fetching price above limit: %s", s
|
||||
);
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function testOperationsAboveLimit(
|
||||
ISwapAdapter adapter,
|
||||
bytes32 poolId,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 amountAboveLimit
|
||||
) internal {
|
||||
console2.log("TEST: Testing operations above the sell limit");
|
||||
uint256[] memory aboveLimitArray = new uint256[](1);
|
||||
aboveLimitArray[0] = amountAboveLimit;
|
||||
|
||||
adapter.price(poolId, tokenIn, tokenOut, aboveLimitArray);
|
||||
adapter.swap(
|
||||
poolId, tokenIn, tokenOut, OrderSide.Sell, aboveLimitArray[0]
|
||||
);
|
||||
}
|
||||
|
||||
function calculateTestAmounts(uint256 limit)
|
||||
internal
|
||||
pure
|
||||
returns (uint256[] memory)
|
||||
{
|
||||
uint256[] memory amounts = new uint256[](4);
|
||||
amounts[0] = 0;
|
||||
amounts[1] = limit / 10000;
|
||||
amounts[2] = limit / 2;
|
||||
amounts[3] = limit;
|
||||
return amounts;
|
||||
}
|
||||
|
||||
function fractionToInt(Fraction memory price)
|
||||
public
|
||||
pure
|
||||
returns (uint256)
|
||||
{
|
||||
return price.numerator * pricePrecision / price.denominator;
|
||||
}
|
||||
|
||||
function hasCapability(
|
||||
Capability[] memory capabilities,
|
||||
Capability capability
|
||||
) internal pure returns (bool) {
|
||||
for (uint256 i = 0; i < capabilities.length; i++) {
|
||||
if (capabilities[i] == capability) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import "./AdapterTest.sol";
|
||||
import {
|
||||
BalancerV2SwapAdapter,
|
||||
IERC20,
|
||||
IVault
|
||||
} from "src/balancer-v2/BalancerV2SwapAdapter.sol";
|
||||
import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol";
|
||||
import {FractionMath} from "src/libraries/FractionMath.sol";
|
||||
|
||||
contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes {
|
||||
contract BalancerV2SwapAdapterTest is AdapterTest {
|
||||
using FractionMath for Fraction;
|
||||
|
||||
IVault constant balancerV2Vault =
|
||||
@@ -229,4 +228,10 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes {
|
||||
);
|
||||
adapter.getPoolIds(100, 200);
|
||||
}
|
||||
|
||||
function testBalancerV2PoolBehaviour() public {
|
||||
bytes32[] memory poolIds = new bytes32[](1);
|
||||
poolIds[0] = B_80BAL_20WETH_POOL_ID;
|
||||
testPoolBehaviour(adapter, poolIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import "./AdapterTest.sol";
|
||||
import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
||||
import "src/uniswap-v2/UniswapV2SwapAdapter.sol";
|
||||
import "src/interfaces/ISwapAdapterTypes.sol";
|
||||
import "src/libraries/FractionMath.sol";
|
||||
|
||||
contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes {
|
||||
contract UniswapV2PairFunctionTest is AdapterTest {
|
||||
using FractionMath for Fraction;
|
||||
|
||||
UniswapV2SwapAdapter adapter;
|
||||
@@ -160,4 +160,10 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes {
|
||||
|
||||
assertEq(limits.length, 2);
|
||||
}
|
||||
|
||||
function testUsv2PoolBehaviour() public {
|
||||
bytes32[] memory poolIds = new bytes32[](1);
|
||||
poolIds[0] = bytes32(bytes20(USDC_WETH_PAIR));
|
||||
testPoolBehaviour(adapter, poolIds);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user