diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index 77b75c0..d7b5c2f 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -3,18 +3,31 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import "forge-std/Test.sol"; -// Maximum Swap In/Out Ratio - 0.3 https://balancer.gitbook.io/balancer/core-concepts/protocol/limitations#v2-limits -uint256 constant RESERVE_LIMIT_FACTOR = 4; +// Maximum Swap In/Out Ratio - 0.3 +// https://balancer.gitbook.io/balancer/core-concepts/protocol/limitations#v2-limits +uint256 constant RESERVE_LIMIT_FACTOR = 4; uint256 constant SWAP_DEADLINE_SEC = 1000; -contract BalancerV2SwapAdapter is ISwapAdapter { +contract BalancerV2SwapAdapter is ISwapAdapter, Test { IVault immutable vault; constructor(address payable vault_) { vault = IVault(vault_); } + /// @notice Calculate the price of the buy token in terms of the sell token. + /// @dev The resulting price is not scaled by the token decimals. + /// Also this function is not 'view' because Balancer V2 simulates the swap + /// and + /// then returns the amount diff in revert data. + /// @param pairId The ID of the trading pool. + /// @param sellToken The token being sold. + /// @param buyToken The token being bought. + /// @param sellAmount The amount of tokens being sold. + /// @return calculatedPrice The price of the buy token in terms of the sell + /// as a Fraction struct. function priceSingle( bytes32 pairId, IERC20 sellToken, @@ -44,9 +57,11 @@ contract BalancerV2SwapAdapter is ISwapAdapter { assetDeltas = vault.queryBatchSwap( IVault.SwapKind.GIVEN_IN, swapSteps, assets, funds ); - - // TODO: the delta of buyToken is negative, so we need to flip the sign - calculatedPrice = Fraction(uint256(assetDeltas[1]), sellAmount); + // assetDeltas[1] is the amount of tokens sent from the vault (i.e. + // bought), so the sign is negative, which means the sign should be + // flipped to get the price. + calculatedPrice = + Fraction(uint256(-assetDeltas[1]), uint256(assetDeltas[0])); } function getSellAmount( @@ -119,7 +134,8 @@ contract BalancerV2SwapAdapter is ISwapAdapter { limit = 0; } else { kind = IVault.SwapKind.GIVEN_OUT; - sellAmount = getSellAmount(pairId, sellToken, buyToken, specifiedAmount); + sellAmount = + getSellAmount(pairId, sellToken, buyToken, specifiedAmount); limit = type(uint256).max; } @@ -146,7 +162,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter { block.timestamp + SWAP_DEADLINE_SEC ); trade.gasUsed = gasBefore - gasleft(); - trade.price = Fraction(0, 1); // Without the price function return 0. + trade.price = priceSingle(pairId, sellToken, buyToken, specifiedAmount); } function getLimits(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) diff --git a/evm/src/libraries/FractionMath.sol b/evm/src/libraries/FractionMath.sol new file mode 100644 index 0000000..43f2ef7 --- /dev/null +++ b/evm/src/libraries/FractionMath.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "src/interfaces/ISwapAdapterTypes.sol"; + +library FractionMath { + /// @dev Compares two Fraction instances from ISwapAdapterTypes. + /// @param frac1 The first Fraction instance. + /// @param frac2 The second Fraction instance. + /// @return int8 Returns 0 if fractions are equal, 1 if frac1 is greater, -1 + /// if frac1 is lesser. + function compareFractions( + ISwapAdapterTypes.Fraction memory frac1, + ISwapAdapterTypes.Fraction memory frac2 + ) internal pure returns (int8) { + uint256 crossProduct1 = frac1.numerator * frac2.denominator; + uint256 crossProduct2 = frac2.numerator * frac1.denominator; + + // fractions are equal + if (crossProduct1 == crossProduct2) return 0; + // frac1 is greater than frac2 + else if (crossProduct1 > crossProduct2) return 1; + // frac1 is less than frac2 + else return -1; + } +} diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index 4aa1e08..ccf3447 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -8,8 +8,11 @@ import { 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 { + using FractionMath for Fraction; + IVault constant balancerV2Vault = IVault(payable(0xBA12222222228d8Ba445958a75a0704d566BF2C8)); BalancerV2SwapAdapter adapter; @@ -20,6 +23,8 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { bytes32 constant B_80BAL_20WETH_POOL_ID = 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014; + uint256 constant TEST_ITERATIONS = 100; + function setUp() public { uint256 forkBlock = 18710000; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); @@ -45,28 +50,50 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { 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); + function testPriceSingleFuzz() public { + uint256 specifiedAmount = 100 * 10 ** 18; + // Assume OrderSide.Sell + uint256[] memory limits = + adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); - // uint256[] memory amounts = new uint256[](1); - // amounts[0] = amount; + vm.assume(specifiedAmount > 0); + vm.assume(specifiedAmount < limits[0]); - // Fraction memory price = - // adapter.priceSingle(B_80BAL_20WETH_POOL_ID, BAL, WETH, amount); + Fraction memory price = adapter.priceSingle( + B_80BAL_20WETH_POOL_ID, BAL, WETH, specifiedAmount + ); - // console.log("price.numerator: ", price.numerator); - // console.log("price.denominator: ", price.denominator); + assertGt(price.numerator, 0); + assertGt(price.denominator, 0); + } - // assertGt(price.numerator, 0); - // } + function testPriceDecreasing() public { + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + Fraction[] memory prices = new Fraction[](TEST_ITERATIONS); + + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { + amounts[i] = 1000 * (i + 1) * 10 ** 18; + console.log("i = ", i); + console.log("amounts[i] = ", amounts[i]); + prices[i] = adapter.priceSingle( + B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts[i] + ); + console.log("prices = ", prices[i].numerator, prices[i].denominator); + } + + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + assertEq(prices[i].compareFractions(prices[i + 1]), 1); + assertGt(prices[i].denominator, 0); + assertGt(prices[i + 1].denominator, 0); + } + } function testSwapFuzz(uint256 specifiedAmount, bool isBuy) public { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; vm.assume(specifiedAmount > 0); - uint256[] memory limits = adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); + uint256[] memory limits = + adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1]); @@ -74,30 +101,98 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { // sellAmount is not specified for buy orders deal(address(BAL), address(this), type(uint256).max); BAL.approve(address(adapter), type(uint256).max); - } - else { + } else { vm.assume(specifiedAmount < limits[0]); deal(address(BAL), address(this), specifiedAmount); BAL.approve(address(adapter), specifiedAmount); } - + uint256 bal_balance = BAL.balanceOf(address(this)); uint256 weth_balance = WETH.balanceOf(address(this)); - Trade memory trade = adapter.swap(B_80BAL_20WETH_POOL_ID, BAL, WETH, side, specifiedAmount); + Trade memory trade = adapter.swap( + B_80BAL_20WETH_POOL_ID, BAL, WETH, side, specifiedAmount + ); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { - assertEq(specifiedAmount, WETH.balanceOf(address(this)) - weth_balance); - assertEq(trade.calculatedAmount, bal_balance - BAL.balanceOf(address(this))); + assertEq( + specifiedAmount, + WETH.balanceOf(address(this)) - weth_balance + ); + assertEq( + trade.calculatedAmount, + bal_balance - BAL.balanceOf(address(this)) + ); } else { - assertEq(specifiedAmount, bal_balance - BAL.balanceOf(address(this))); - assertEq(trade.calculatedAmount, WETH.balanceOf(address(this)) - weth_balance); + assertEq( + specifiedAmount, bal_balance - BAL.balanceOf(address(this)) + ); + assertEq( + trade.calculatedAmount, + WETH.balanceOf(address(this)) - weth_balance + ); } } } + function testSwapSellIncreasing() public { + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + Trade[] memory trades = new Trade[](TEST_ITERATIONS); + + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { + amounts[i] = 1000 * (i + 1) * 10 ** 18; + + uint256 beforeSwap = vm.snapshot(); + + deal(address(BAL), address(this), amounts[i]); + BAL.approve(address(adapter), amounts[i]); + trades[i] = adapter.swap( + B_80BAL_20WETH_POOL_ID, BAL, WETH, OrderSide.Sell, amounts[i] + ); + + vm.revertTo(beforeSwap); + } + + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); + assertLe(trades[i].gasUsed, trades[i + 1].gasUsed); + assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1); + } + } + + function testSwapBuyIncreasing() public { + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + Trade[] memory trades = new Trade[](TEST_ITERATIONS); + + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { + amounts[i] = 10 * (i + 1) * 10 ** 18; + + uint256 beforeSwap = vm.snapshot(); + + Fraction memory price = adapter.priceSingle( + B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts[i] + ); + uint256 amountIn = + (amounts[i] * price.denominator / price.numerator) * 2; + + deal(address(BAL), address(this), amountIn); + BAL.approve(address(adapter), amountIn); + trades[i] = adapter.swap( + B_80BAL_20WETH_POOL_ID, BAL, WETH, OrderSide.Buy, amounts[i] + ); + + vm.revertTo(beforeSwap); + } + + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); + assertLe(trades[i].gasUsed, trades[i + 1].gasUsed); + assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1); + } + } + function testGetLimits() public view { uint256[] memory limits = adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); @@ -107,7 +202,9 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { assert(limits[1] > 0); } - function testGetCapabilitiesFuzz(bytes32 pair, address t0, address t1) public { + function testGetCapabilitiesFuzz(bytes32 pair, address t0, address t1) + public + { Capability[] memory res = adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index c160dc7..fa4970a 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -5,13 +5,18 @@ import "forge-std/Test.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 { + using FractionMath for Fraction; + UniswapV2SwapAdapter pairFunctions; IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); address constant USDC_WETH_PAIR = 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc; + uint256 constant TEST_ITERATIONS = 100; + function setUp() public { uint256 forkBlock = 17000000; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); @@ -45,38 +50,22 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testPriceDecreasing() public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - uint256[] memory amounts = new uint256[](100); + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); - for (uint256 i = 0; i < 100; i++) { + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { amounts[i] = 1000 * i * 10 ** 6; } Fraction[] memory prices = pairFunctions.price(pair, WETH, USDC, amounts); - for (uint256 i = 0; i < 99; i++) { - assertEq(compareFractions(prices[i], prices[i + 1]), 1); + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + assertEq(prices[i].compareFractions(prices[i + 1]), 1); assertGt(prices[i].denominator, 0); assertGt(prices[i + 1].denominator, 0); } } - function compareFractions(Fraction memory frac1, Fraction memory frac2) - internal - pure - returns (int8) - { - uint256 crossProduct1 = frac1.numerator * frac2.denominator; - uint256 crossProduct2 = frac2.numerator * frac1.denominator; - - // fractions are equal - if (crossProduct1 == crossProduct2) return 0; - // frac1 is greater than frac2 - else if (crossProduct1 > crossProduct2) return 1; - // frac1 is less than frac2 - else return -1; - } - function testSwapFuzz(uint256 specifiedAmount, bool isBuy) public { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; @@ -89,8 +78,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { // sellAmount is not specified for buy orders deal(address(USDC), address(this), type(uint256).max); USDC.approve(address(pairFunctions), type(uint256).max); - } - else { + } else { vm.assume(specifiedAmount < limits[0]); deal(address(USDC), address(this), specifiedAmount); @@ -100,15 +88,28 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { uint256 usdc_balance = USDC.balanceOf(address(this)); uint256 weth_balance = WETH.balanceOf(address(this)); - Trade memory trade = pairFunctions.swap(pair, USDC, WETH, side, specifiedAmount); + Trade memory trade = + pairFunctions.swap(pair, USDC, WETH, side, specifiedAmount); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { - assertEq(specifiedAmount, WETH.balanceOf(address(this)) - weth_balance); - assertEq(trade.calculatedAmount, usdc_balance - USDC.balanceOf(address(this))); + assertEq( + specifiedAmount, + WETH.balanceOf(address(this)) - weth_balance + ); + assertEq( + trade.calculatedAmount, + usdc_balance - USDC.balanceOf(address(this)) + ); } else { - assertEq(specifiedAmount, usdc_balance - USDC.balanceOf(address(this))); - assertEq(trade.calculatedAmount, WETH.balanceOf(address(this)) - weth_balance); + assertEq( + specifiedAmount, + usdc_balance - USDC.balanceOf(address(this)) + ); + assertEq( + trade.calculatedAmount, + WETH.balanceOf(address(this)) - weth_balance + ); } } } @@ -119,16 +120,15 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function executeIncreasingSwaps(OrderSide side) internal { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - uint256 iterations = 100; - uint256[] memory amounts = new uint256[](iterations); - for (uint256 i = 0; i < 100; i++) { + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { amounts[i] = 1000 * i * 10 ** 6; } - Trade[] memory trades = new Trade[](iterations); + Trade[] memory trades = new Trade[](TEST_ITERATIONS); uint256 beforeSwap; - for (uint256 i = 0; i < iterations; i++) { + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { beforeSwap = vm.snapshot(); deal(address(USDC), address(this), amounts[i]); @@ -138,10 +138,10 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { vm.revertTo(beforeSwap); } - for (uint256 i = 1; i < iterations - 1; i++) { + for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) { assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); assertLe(trades[i].gasUsed, trades[i + 1].gasUsed); - assertEq(compareFractions(trades[i].price, trades[i + 1].price), 1); + assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1); } }