diff --git a/docs/logic/vm-integration/ethereum-solidity.md b/docs/logic/vm-integration/ethereum-solidity.md index 0946a56..9e8926d 100644 --- a/docs/logic/vm-integration/ethereum-solidity.md +++ b/docs/logic/vm-integration/ethereum-solidity.md @@ -93,7 +93,7 @@ function swap( bytes32 pairId, IERC20 sellToken, IERC20 buyToken, - SwapSide side, + OrderSide side, uint256 specifiedAmount ) external returns (Trade memory trade); ``` @@ -105,7 +105,7 @@ Retrieves the limits for each token. This method returns the maximum limits of a token that can be traded. The limit is reached when the change in the received amounts is zero or close to zero. If in doubt over estimate the limit. The swap function should not error with LimitExceeded if called with any amounts below the limit. ```solidity -function getLimits(bytes32 pairId, SwapSide side) +function getLimits(bytes32 pairId, OrderSide side) external returns (uint256[] memory); ``` diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index da182f6..1250cd9 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -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 + } +} diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol index bc7ebe4..f93bed6 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -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); diff --git a/evm/src/interfaces/ISwapAdapterTypes.sol b/evm/src/interfaces/ISwapAdapterTypes.sol index d6a5f73..0fa0f84 100644 --- a/evm/src/interfaces/ISwapAdapterTypes.sol +++ b/evm/src/interfaces/ISwapAdapterTypes.sol @@ -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; diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index 9d4ef9e..dab63ac 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -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; } diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index f662972..116b920 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -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); } }