Fix UniswapV2 tests

This commit is contained in:
pistomat
2023-12-04 13:50:02 +01:00
parent 0fa898c69d
commit 7ffee9ac2e
6 changed files with 347 additions and 109 deletions

View File

@@ -1,66 +1,13 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma experimental ABIEncoderV2;
pragma solidity ^0.8.13;
import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
interface IVault {
function getPoolTokens(bytes32 poolId)
external
view
returns (
IERC20[] memory tokens,
uint256[] memory balances,
uint256 lastChangeBlock
);
function swap(
SingleSwap memory singleSwap,
FundManagement memory funds,
uint256 limit,
uint256 deadline
) external payable returns (uint256);
function queryBatchSwap(
SwapKind kind,
BatchSwapStep[] memory swaps,
IAsset[] memory assets,
FundManagement memory funds
) external returns (int256[] memory assetDeltas);
struct SingleSwap {
bytes32 poolId;
SwapKind kind;
IAsset assetIn;
IAsset assetOut;
uint256 amount;
bytes userData;
}
struct BatchSwapStep {
bytes32 poolId;
uint256 assetInIndex;
uint256 assetOutIndex;
uint256 amount;
bytes userData;
}
struct FundManagement {
address sender;
bool fromInternalBalance;
address payable recipient;
bool toInternalBalance;
}
enum SwapKind { GIVEN_IN, GIVEN_OUT }
}
interface IAsset is IERC20 {
}
contract BalancerV2SwapAdapter is ISwapAdapter {
IVault immutable vault;
constructor(address vault_) {
constructor(address payable vault_) {
vault = IVault(vault_);
}
@@ -73,31 +20,59 @@ contract BalancerV2SwapAdapter is ISwapAdapter {
revert NotImplemented("BalancerV2SwapAdapter.price");
}
function getPriceAt(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
internal
pure
returns (Fraction memory)
{
revert NotImplemented("BalancerV2SwapAdapter.getPriceAt");
}
function swap(
bytes32 pairId,
IERC20 sellToken,
IERC20 buyToken,
SwapSide side,
OrderSide side,
uint256 specifiedAmount
) external override returns (Trade memory trade) {
revert NotImplemented("BalancerV2SwapAdapter.swap");
if (side == OrderSide.Sell) {
sellToken.approve(address(vault), specifiedAmount);
} else {
buyToken.approve(address(vault), type(uint256).max);
}
uint256 gasBefore = gasleft();
trade.receivedAmount = vault.swap(
IVault.SingleSwap({
poolId: pairId,
kind: side == OrderSide.Sell
? IVault.SwapKind.GIVEN_IN
: IVault.SwapKind.GIVEN_OUT,
assetIn: address(sellToken),
assetOut: address(buyToken),
amount: specifiedAmount,
userData: ""
}),
IVault.FundManagement({
sender: msg.sender,
fromInternalBalance: false,
recipient: payable(msg.sender),
toInternalBalance: false
}),
0,
block.number
);
trade.gasUsed = gasBefore - gasleft();
trade.price = Fraction(0, 1); // Without the price function return 0.
}
function getLimits(bytes32 pairId, SwapSide side)
function getLimits(bytes32 pairId, IERC20 sellToken, IERC20 buyToken)
external
view
override
returns (uint256[] memory limits)
{
(, limits, ) = vault.getPoolTokens(pairId);
(IERC20[] memory tokens, uint256[] memory balances,) =
vault.getPoolTokens(pairId);
for (uint256 i = 0; i < tokens.length; i++) {
if (tokens[i] == sellToken) {
limits[0] = balances[i];
}
if (tokens[i] == buyToken) {
limits[1] = balances[i];
}
}
}
function getCapabilities(bytes32, IERC20, IERC20)
@@ -106,10 +81,9 @@ contract BalancerV2SwapAdapter is ISwapAdapter {
override
returns (Capability[] memory capabilities)
{
capabilities = new Capability[](3);
capabilities[0] = Capability.SellSide;
capabilities[1] = Capability.BuySide;
capabilities[2] = Capability.PriceFunction;
capabilities = new Capability[](2);
capabilities[0] = Capability.SellOrder;
capabilities[1] = Capability.BuyOrder;
}
function getTokens(bytes32 pairId)
@@ -118,10 +92,10 @@ contract BalancerV2SwapAdapter is ISwapAdapter {
override
returns (IERC20[] memory tokens)
{
revert NotImplemented("BalancerV2SwapAdapter.getTokens");
(tokens,,) = vault.getPoolTokens(pairId);
}
/// @dev Balancer V2 does not support listing pools.
/// @dev Balancer V2 does not support enumerating pools, they have to be indexed off-chain.
function getPoolIds(uint256 offset, uint256 limit)
external
view
@@ -131,3 +105,261 @@ contract BalancerV2SwapAdapter is ISwapAdapter {
revert NotImplemented("BalancerV2SwapAdapter.getPoolIds");
}
}
interface IVault {
struct BatchSwapStep {
bytes32 poolId;
uint256 assetInIndex;
uint256 assetOutIndex;
uint256 amount;
bytes userData;
}
struct FundManagement {
address sender;
bool fromInternalBalance;
address recipient;
bool toInternalBalance;
}
struct ExitPoolRequest {
address[] assets;
uint256[] minAmountsOut;
bytes userData;
bool toInternalBalance;
}
struct JoinPoolRequest {
address[] assets;
uint256[] maxAmountsIn;
bytes userData;
bool fromInternalBalance;
}
struct PoolBalanceOp {
SwapKind kind;
bytes32 poolId;
address token;
uint256 amount;
}
struct UserBalanceOp {
SwapKind kind;
address asset;
uint256 amount;
address sender;
address recipient;
}
struct SingleSwap {
bytes32 poolId;
SwapKind kind;
address assetIn;
address assetOut;
uint256 amount;
bytes userData;
}
event AuthorizerChanged(address indexed newAuthorizer);
event ExternalBalanceTransfer(
address indexed token,
address indexed sender,
address recipient,
uint256 amount
);
event FlashLoan(
address indexed recipient,
address indexed token,
uint256 amount,
uint256 feeAmount
);
event InternalBalanceChanged(
address indexed user,
address indexed token,
int256 delta
);
event PausedStateChanged(bool paused);
event PoolBalanceChanged(
bytes32 indexed poolId,
address indexed liquidityProvider,
address[] tokens,
int256[] deltas,
uint256[] protocolFeeAmounts
);
event PoolBalanceManaged(
bytes32 indexed poolId,
address indexed assetManager,
address indexed token,
int256 cashDelta,
int256 managedDelta
);
event PoolRegistered(
bytes32 indexed poolId,
address indexed poolAddress,
uint8 specialization
);
event RelayerApprovalChanged(
address indexed relayer,
address indexed sender,
bool approved
);
event Swap(
bytes32 indexed poolId,
address indexed tokenIn,
address indexed tokenOut,
uint256 amountIn,
uint256 amountOut
);
event TokensDeregistered(bytes32 indexed poolId, address[] tokens);
event TokensRegistered(
bytes32 indexed poolId,
address[] tokens,
address[] assetManagers
);
function WETH() external view returns (address);
function batchSwap(
SwapKind kind,
BatchSwapStep[] memory swaps,
address[] memory assets,
FundManagement memory funds,
int256[] memory limits,
uint256 deadline
) external payable returns (int256[] memory assetDeltas);
function deregisterTokens(bytes32 poolId, address[] memory tokens) external;
function exitPool(
bytes32 poolId,
address sender,
address recipient,
ExitPoolRequest memory request
) external;
function flashLoan(
address recipient,
address[] memory tokens,
uint256[] memory amounts,
bytes memory userData
) external;
function getActionId(bytes4 selector) external view returns (bytes32);
function getAuthorizer() external view returns (address);
function getDomainSeparator() external view returns (bytes32);
function getInternalBalance(address user, address[] memory tokens)
external
view
returns (uint256[] memory balances);
function getNextNonce(address user) external view returns (uint256);
function getPausedState()
external
view
returns (
bool paused,
uint256 pauseWindowEndTime,
uint256 bufferPeriodEndTime
);
function getPool(bytes32 poolId) external view returns (address, uint8);
function getPoolTokenInfo(bytes32 poolId, address token)
external
view
returns (
uint256 cash,
uint256 managed,
uint256 lastChangeBlock,
address assetManager
);
function getProtocolFeesCollector() external view returns (address);
function hasApprovedRelayer(address user, address relayer)
external
view
returns (bool);
function joinPool(
bytes32 poolId,
address sender,
address recipient,
JoinPoolRequest memory request
) external payable;
function managePoolBalance(PoolBalanceOp[] memory ops) external;
function manageUserBalance(UserBalanceOp[] memory ops) external payable;
function queryBatchSwap(
SwapKind kind,
BatchSwapStep[] memory swaps,
address[] memory assets,
FundManagement memory funds
) external returns (int256[] memory);
function registerPool(uint8 specialization) external returns (bytes32);
function registerTokens(
bytes32 poolId,
address[] memory tokens,
address[] memory assetManagers
) external;
function setAuthorizer(address newAuthorizer) external;
function setPaused(bool paused) external;
function setRelayerApproval(
address sender,
address relayer,
bool approved
) external;
/**
* @dev Performs a swap with a single Pool.
*
* If the swap is 'given in' (the number of tokens to send to the Pool is
* known), it returns the amount of tokens
* taken from the Pool, which must be greater than or equal to `limit`.
*
* If the swap is 'given out' (the number of tokens to take from the Pool is
* known), it returns the amount of tokens
* sent to the Pool, which must be less than or equal to `limit`.
*
* Internal Balance usage and the recipient are determined by the `funds`
* struct.
*
* Emits a `Swap` event.
*/
function swap(
SingleSwap memory singleSwap,
FundManagement memory funds,
uint256 limit,
uint256 deadline
) external payable returns (uint256);
receive() external payable;
function getPoolTokens(bytes32 poolId)
external
view
returns (
IERC20[] memory tokens,
uint256[] memory balances,
uint256 lastChangeBlock
);
enum SwapKind
{
/// The number of tokens to send to the Pool is known
GIVEN_IN,
/// The number of tokens to take from the Pool is known
GIVEN_OUT
}
}

View File

@@ -60,7 +60,7 @@ interface ISwapAdapter is ISwapAdapterTypes {
bytes32 pairId,
IERC20 sellToken,
IERC20 buyToken,
SwapSide side,
OrderSide side,
uint256 specifiedAmount
) external returns (Trade memory trade);
@@ -71,9 +71,10 @@ interface ISwapAdapter is ISwapAdapterTypes {
/// swap function should not error with `LimitExceeded` if called with
/// amounts below the limit.
/// @param pairId The ID of the trading pair.
/// @param side The side of the trade (Sell or Buy).
/// @param sellToken The token being sold.
/// @param buyToken The token being bought.
/// @return limits An array of limits.
function getLimits(bytes32 pairId, SwapSide side)
function getLimits(bytes32 pairId, IERC20 sellToken, IERC20 buyToken)
external
returns (uint256[] memory limits);

View File

@@ -4,10 +4,10 @@ pragma solidity ^0.8.13;
import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
interface ISwapAdapterTypes {
/// @dev The SwapSide enum represents possible sides of a trade: Sell or
/// Buy. E.g. if SwapSide is Sell, the sell amount is interpreted to be
/// @dev The OrderSide enum represents possible sides of a trade: Sell or
/// Buy. E.g. if OrderSide is Sell, the sell amount is interpreted to be
/// fixed.
enum SwapSide {
enum OrderSide {
Sell,
Buy
}
@@ -16,10 +16,10 @@ interface ISwapAdapterTypes {
/// pair.
enum Capability {
Unset,
// Support SwapSide.Sell values (required)
SellSide,
// Support SwapSide.Buy values (optional)
BuySide,
// Support OrderSide.Sell values (required)
SellOrder,
// Support OrderSide.Buy values (optional)
BuyOrder,
// Support evaluating the price function (optional)
PriceFunction,
// Support tokens that charge a fee on transfer (optional)
@@ -44,7 +44,8 @@ interface ISwapAdapterTypes {
/// @dev The Trade struct holds data about an executed trade.
struct Trade {
// The amount received from the trade.
// If the side is sell, it is the amount of tokens sold. If the side is
// buy, it is the amount of tokens bought.
uint256 receivedAmount;
// The amount of gas used in the trade.
uint256 gasUsed;

View File

@@ -3,7 +3,7 @@ pragma solidity ^0.8.13;
import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
uint256 constant RESERVE_LIMIT_FACTOR = 10;
uint256 constant RESERVE_LIMIT_FACTOR = 10; // TODO why is the factor so high?
contract UniswapV2SwapAdapter is ISwapAdapter {
IUniswapV2Factory immutable factory;
@@ -54,11 +54,12 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
bytes32 pairId,
IERC20 sellToken,
IERC20 buyToken,
SwapSide side,
OrderSide side,
uint256 specifiedAmount
) external override returns (Trade memory trade) {
if (specifiedAmount == 0) {
return trade; // TODO: This returns Fraction(0, 0) instead of the expected zero Fraction(0, 1)
return trade; // TODO: This returns Fraction(0, 0) instead of the
// expected zero Fraction(0, 1)
}
IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId)));
@@ -71,7 +72,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
(r1, r0,) = pair.getReserves();
}
uint256 gasBefore = gasleft();
if (side == SwapSide.Sell) {
if (side == OrderSide.Sell) {
trade.receivedAmount =
sell(pair, sellToken, zero2one, r0, r1, specifiedAmount);
} else {
@@ -154,15 +155,18 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
if (amountIn == 0) {
return 0;
}
if (reserveIn == 0 || reserveOut == 0) {
revert Unavailable("At least one reserve is zero!");
if (reserveIn == 0) {
revert Unavailable("reserveIn is zero");
}
if (reserveOut == 0) {
revert Unavailable("reserveOut is zero");
}
uint256 numerator = reserveIn * amountOut * 1000;
uint256 denominator = (reserveOut - amountOut) * 997;
amountIn = (numerator / denominator) + 1;
}
function getLimits(bytes32 pairId, SwapSide side)
function getLimits(bytes32 pairId, IERC20 sellToken, IERC20 buyToken)
external
view
override
@@ -170,13 +174,10 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
{
IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId)));
limits = new uint256[](2);
(uint256 r0, uint256 r1,) = pair.getReserves();
if (side == SwapSide.Sell) {
limits[0] = r0 * RESERVE_LIMIT_FACTOR;
limits[1] = r1 * RESERVE_LIMIT_FACTOR;
if (sellToken < buyToken) {
(limits[0], limits[1],) = pair.getReserves();
} else {
limits[0] = r1 * RESERVE_LIMIT_FACTOR;
limits[1] = r0 * RESERVE_LIMIT_FACTOR;
(limits[1], limits[0],) = pair.getReserves();
}
}
@@ -187,8 +188,8 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
returns (Capability[] memory capabilities)
{
capabilities = new Capability[](3);
capabilities[0] = Capability.SellSide;
capabilities[1] = Capability.BuySide;
capabilities[0] = Capability.SellOrder;
capabilities[1] = Capability.BuyOrder;
capabilities[2] = Capability.PriceFunction;
}

View File

@@ -21,7 +21,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes {
function testPriceFuzz(uint256 amount0, uint256 amount1) public {
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
uint256[] memory limits = pairFunctions.getLimits(pair, SwapSide.Sell);
uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH);
vm.assume(amount0 < limits[0]);
vm.assume(amount1 < limits[0]);
@@ -74,11 +74,14 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes {
function testSwapFuzz(uint256 amount, bool isBuy) public {
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
SwapSide side = SwapSide.Sell;
OrderSide side = OrderSide.Sell;
uint256[] memory limits;
if (isBuy) {
side = SwapSide.Buy;
side = OrderSide.Buy;
limits = pairFunctions.getLimits(pair, WETH, USDC);
} else {
limits = pairFunctions.getLimits(pair, USDC, WETH);
}
uint256[] memory limits = pairFunctions.getLimits(pair, side);
vm.assume(amount < limits[0]);
deal(address(USDC), address(this), amount);
USDC.approve(address(pairFunctions), amount);
@@ -87,10 +90,10 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes {
}
function testSwapSellIncreasing() public {
executeIncreasingSwaps(SwapSide.Sell);
executeIncreasingSwaps(OrderSide.Sell);
}
function executeIncreasingSwaps(SwapSide side) internal {
function executeIncreasingSwaps(OrderSide side) internal {
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
uint256[] memory amounts = new uint256[](100);
@@ -116,7 +119,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes {
}
function testSwapBuyIncreasing() public {
executeIncreasingSwaps(SwapSide.Buy);
executeIncreasingSwaps(OrderSide.Buy);
}
function testGetCapabilities(bytes32 pair, address t0, address t1) public {
@@ -128,6 +131,6 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes {
function testGetLimits() public {
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
uint256[] memory limits = pairFunctions.getLimits(pair, SwapSide.Sell);
uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH);
}
}