diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index 1250cd9..f545269 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +uint256 constant RESERVE_LIMIT_FACTOR = 10; // TODO why is the factor so high? +uint256 constant SWAP_DEADLINE_SEC = 1000; + contract BalancerV2SwapAdapter is ISwapAdapter { IVault immutable vault; @@ -11,12 +14,57 @@ contract BalancerV2SwapAdapter is ISwapAdapter { vault = IVault(vault_); } - function price( + function priceSingle( bytes32 pairId, IERC20 sellToken, IERC20 buyToken, - uint256[] memory sellAmounts - ) external view override returns (Fraction[] memory prices) { + uint256 sellAmount + ) public returns (Fraction memory calculatedPrice) { + IVault.BatchSwapStep[] memory swapSteps = new IVault.BatchSwapStep[](1); + swapSteps[0] = IVault.BatchSwapStep({ + poolId: pairId, + assetInIndex: 0, + assetOutIndex: 1, + amount: sellAmount, + userData: "" + }); + address[] memory assets = new address[](2); + assets[0] = address(sellToken); + assets[1] = address(buyToken); + IVault.FundManagement memory funds = IVault.FundManagement({ + sender: msg.sender, + fromInternalBalance: false, + recipient: payable(msg.sender), + toInternalBalance: false + }); + + // assetDeltas correspond to the assets array + int256[] memory assetDeltas = new int256[](2); + assetDeltas = vault.queryBatchSwap( + IVault.SwapKind.GIVEN_IN, swapSteps, assets, funds + ); + + calculatedPrice = Fraction(uint256(assetDeltas[1]), sellAmount); + } + + function priceBatch( + bytes32 pairId, + IERC20 sellToken, + IERC20 buyToken, + uint256[] memory specifiedAmounts + ) external returns (Fraction[] memory calculatedPrices) { + for (uint256 i = 0; i < specifiedAmounts.length; i++) { + calculatedPrices[i] = + priceSingle(pairId, sellToken, buyToken, specifiedAmounts[i]); + } + } + + function price(bytes32, IERC20, IERC20, uint256[] memory) + external + pure + override + returns (Fraction[] memory) + { revert NotImplemented("BalancerV2SwapAdapter.price"); } @@ -44,14 +92,15 @@ contract BalancerV2SwapAdapter is ISwapAdapter { amount: specifiedAmount, userData: "" }), + // This contract is not an approved relayer (yet), so the sender and recipient cannot be msg.sender IVault.FundManagement({ - sender: msg.sender, + sender: address(this), fromInternalBalance: false, - recipient: payable(msg.sender), + recipient: payable(address(this)), toInternalBalance: false }), 0, - block.number + block.timestamp + SWAP_DEADLINE_SEC ); trade.gasUsed = gasBefore - gasleft(); trade.price = Fraction(0, 1); // Without the price function return 0. @@ -63,14 +112,16 @@ contract BalancerV2SwapAdapter is ISwapAdapter { override returns (uint256[] memory limits) { + limits = new uint256[](2); (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]; + limits[0] = balances[i] * RESERVE_LIMIT_FACTOR; } if (tokens[i] == buyToken) { - limits[1] = balances[i]; + limits[1] = balances[i] * RESERVE_LIMIT_FACTOR; } } } @@ -95,12 +146,13 @@ contract BalancerV2SwapAdapter is ISwapAdapter { (tokens,,) = vault.getPoolTokens(pairId); } - /// @dev Balancer V2 does not support enumerating pools, they have to be indexed off-chain. - function getPoolIds(uint256 offset, uint256 limit) + /// @dev Balancer V2 does not support enumerating pools, they have to be + /// indexed off-chain. + function getPoolIds(uint256, uint256) external - view + pure override - returns (bytes32[] memory ids) + returns (bytes32[] memory) { revert NotImplemented("BalancerV2SwapAdapter.getPoolIds"); } @@ -174,9 +226,7 @@ interface IVault { uint256 feeAmount ); event InternalBalanceChanged( - address indexed user, - address indexed token, - int256 delta + address indexed user, address indexed token, int256 delta ); event PausedStateChanged(bool paused); event PoolBalanceChanged( @@ -199,9 +249,7 @@ interface IVault { uint8 specialization ); event RelayerApprovalChanged( - address indexed relayer, - address indexed sender, - bool approved + address indexed relayer, address indexed sender, bool approved ); event Swap( bytes32 indexed poolId, @@ -212,9 +260,7 @@ interface IVault { ); event TokensDeregistered(bytes32 indexed poolId, address[] tokens); event TokensRegistered( - bytes32 indexed poolId, - address[] tokens, - address[] assetManagers + bytes32 indexed poolId, address[] tokens, address[] assetManagers ); function WETH() external view returns (address); @@ -228,7 +274,8 @@ interface IVault { uint256 deadline ) external payable returns (int256[] memory assetDeltas); - function deregisterTokens(bytes32 poolId, address[] memory tokens) external; + function deregisterTokens(bytes32 poolId, address[] memory tokens) + external; function exitPool( bytes32 poolId, @@ -296,6 +343,28 @@ interface IVault { function manageUserBalance(UserBalanceOp[] memory ops) external payable; + /** + * @dev Simulates a call to `batchSwap`, returning an array of Vault asset + * deltas. Calls to `swap` cannot be + * simulated directly, but an equivalent `batchSwap` call can and will yield + * the exact same result. + * + * Each element in the array corresponds to the asset at the same index, and + * indicates the number of tokens (or ETH) + * the Vault would take from the sender (if positive) or send to the + * recipient (if negative). The arguments it + * receives are the same that an equivalent `batchSwap` call would receive. + * + * Unlike `batchSwap`, this function performs no checks on the sender or + * recipient field in the `funds` struct. + * This makes it suitable to be called by off-chain applications via + * eth_call without needing to hold tokens, + * approve them for the Vault, or even know a user's address. + * + * Note that this function is not 'view' (due to implementation details): + * the client code must explicitly execute + * eth_call instead of eth_sendTransaction. + */ function queryBatchSwap( SwapKind kind, BatchSwapStep[] memory swaps, @@ -315,11 +384,8 @@ interface IVault { function setPaused(bool paused) external; - function setRelayerApproval( - address sender, - address relayer, - bool approved - ) external; + function setRelayerApproval(address sender, address relayer, bool approved) + external; /** * @dev Performs a swap with a single Pool. @@ -345,7 +411,7 @@ interface IVault { ) external payable returns (uint256); receive() external payable; - + function getPoolTokens(bytes32 poolId) external view @@ -356,8 +422,8 @@ interface IVault { ); enum SwapKind + /// The number of tokens to send to the Pool is known { - /// 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 f93bed6..ed8b017 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -29,14 +29,15 @@ interface ISwapAdapter is ISwapAdapterTypes { /// @param pairId The ID of the trading pair. /// @param sellToken The token being sold. /// @param buyToken The token being bought. - /// @param sellAmounts The specified amounts used for price calculation. + /// @param specifiedAmounts The specified amounts used for price + /// calculation. /// @return prices array of prices as fractions corresponding to the /// provided amounts. function price( bytes32 pairId, IERC20 sellToken, IERC20 buyToken, - uint256[] memory sellAmounts + uint256[] memory specifiedAmounts ) external view returns (Fraction[] memory prices); /** diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index dab63ac..fad2924 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -16,9 +16,9 @@ contract UniswapV2SwapAdapter is ISwapAdapter { bytes32 pairId, IERC20 sellToken, IERC20 buyToken, - uint256[] memory sellAmounts + uint256[] memory specifiedAmounts ) external view override returns (Fraction[] memory prices) { - prices = new Fraction[](sellAmounts.length); + prices = new Fraction[](specifiedAmounts.length); IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId))); uint112 r0; uint112 r1; @@ -28,8 +28,8 @@ contract UniswapV2SwapAdapter is ISwapAdapter { (r1, r0,) = pair.getReserves(); } - for (uint256 i = 0; i < sellAmounts.length; i++) { - prices[i] = getPriceAt(sellAmounts[i], r0, r1); + for (uint256 i = 0; i < specifiedAmounts.length; i++) { + prices[i] = getPriceAt(specifiedAmounts[i], r0, r1); } } diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index 64b955a..491bde5 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -1 +1,113 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import { + BalancerV2SwapAdapter, + IERC20, + IVault +} from "src/balancer-v2/BalancerV2SwapAdapter.sol"; +import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol"; + +contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { + IVault constant balancerV2Vault = + IVault(payable(0xBA12222222228d8Ba445958a75a0704d566BF2C8)); + BalancerV2SwapAdapter adapter; + + IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + IERC20 constant BAL = IERC20(0xba100000625a3754423978a60c9317c58a424e3D); + address constant B_80BAL_20WETH = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; + bytes32 constant B_80BAL_20WETH_POOL_ID = + 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014; + + function setUp() public { + uint256 forkBlock = 17000000; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + + adapter = new BalancerV2SwapAdapter(payable(address(balancerV2Vault))); + + vm.label(address(balancerV2Vault), "IVault"); + vm.label(address(adapter), "BalancerV2SwapAdapter"); + vm.label(address(WETH), "WETH"); + vm.label(address(BAL), "BAL"); + vm.label(address(B_80BAL_20WETH), "B_80BAL_20WETH"); + } + + function testPrice() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 100; + amounts[1] = 200; + vm.expectRevert( + abi.encodeWithSelector( + NotImplemented.selector, "BalancerV2SwapAdapter.price" + ) + ); + adapter.price(B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts); + } + + // function testPriceSingleFuzz(uint256 amount) public { + // uint256[] memory limits = adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); + // vm.assume(amount < limits[0]); + // vm.assume(amount > 100); + + // uint256[] memory amounts = new uint256[](1); + // amounts[0] = amount; + + // Fraction memory price = + // adapter.priceSingle(B_80BAL_20WETH_POOL_ID, BAL, WETH, amount); + + // console.log("price.numerator: ", price.numerator); + // console.log("price.denominator: ", price.denominator); + + // assertGt(price.numerator, 0); + // } + + function testSwapFuzz() public { + // uint256[] memory limits = adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); + // vm.assume(amount < limits[0]); + // vm.assume(amount > 1000000); // TODO getting reverts for amounts near zero + uint256 amount = 100000; + + OrderSide side = OrderSide.Sell; + + deal(address(BAL), address(adapter), amount); + // BAL.approve(address(adapter), amount); + // BAL.approve(address(balancerV2Vault), amount); + + adapter.swap(B_80BAL_20WETH_POOL_ID, BAL, WETH, side, amount); + } + + function testGetLimits() public view { + uint256[] memory limits = + adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); + + assert(limits.length == 2); + assert(limits[0] > 0); + assert(limits[1] > 0); + } + + function testGetCapabilitiesFuzz(bytes32 pair, address t0, address t1) public { + Capability[] memory res = + adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); + + assertEq(res.length, 2); + assertEq(uint256(res[0]), uint256(Capability.SellOrder)); + assertEq(uint256(res[1]), uint256(Capability.BuyOrder)); + } + + function testGetTokens() public { + IERC20[] memory tokens = adapter.getTokens(B_80BAL_20WETH_POOL_ID); + + assertEq(address(tokens[0]), address(BAL)); + assertEq(address(tokens[1]), address(WETH)); + } + + function testGetPoolIds() public { + vm.expectRevert( + abi.encodeWithSelector( + NotImplemented.selector, "BalancerV2SwapAdapter.getPoolIds" + ) + ); + adapter.getPoolIds(100, 200); + } +} diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index 116b920..9a89e4f 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -74,15 +74,11 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testSwapFuzz(uint256 amount, bool isBuy) public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - OrderSide side = OrderSide.Sell; - uint256[] memory limits; - if (isBuy) { - side = OrderSide.Buy; - limits = pairFunctions.getLimits(pair, WETH, USDC); - } else { - limits = pairFunctions.getLimits(pair, USDC, WETH); - } + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH); vm.assume(amount < limits[0]); + deal(address(USDC), address(this), amount); USDC.approve(address(pairFunctions), amount); @@ -132,5 +128,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testGetLimits() public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH); + + assertEq(limits.length, 2); } }