From c6de3816fd3ec2b1af87ba7ae948b8387d3a28b3 Mon Sep 17 00:00:00 2001 From: Brean0 Date: Tue, 13 Aug 2024 17:34:43 +0200 Subject: [PATCH 01/22] init --- src/functions/Stable2.sol | 41 +++++++++++++++++-- test/TestHelper.sol | 8 ++++ ...kStable2.calcReserveAtRatioLiquidity.t.sol | 18 +++++++- ...nstalkStable2.calcReserveAtRatioSwap.t.sol | 26 +++++++++++- 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/src/functions/Stable2.sol b/src/functions/Stable2.sol index ca25edc0..404f782e 100644 --- a/src/functions/Stable2.sol +++ b/src/functions/Stable2.sol @@ -26,6 +26,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { struct PriceData { uint256 targetPrice; uint256 currentPrice; + uint256 newPrice; uint256 maxStepSize; ILookupTable.PriceData lutData; } @@ -222,8 +223,26 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { // calculate scaledReserve[i]: scaledReserves[i] = calcReserve(scaledReserves, i, lpTokenSupply, abi.encode(18, 18)); - // calc currentPrice: - pd.currentPrice = _calcRate(scaledReserves, i, j, lpTokenSupply); + // calculate new price from reserves: + pd.newPrice = _calcRate(scaledReserves, i, j, lpTokenSupply); + + // if the new current price is either lower or higher than both the previous current price and the target price, + // (i.e the target price lies between the current price and the previous current price), + // recalibrate high/low price. + if (pd.newPrice > pd.currentPrice && pd.newPrice > pd.targetPrice) { + pd.lutData.highPriceJ = scaledReserves[j] * 1e18 / parityReserve; + pd.lutData.highPriceI = scaledReserves[i] * 1e18 / parityReserve; + pd.lutData.highPrice = pd.newPrice; + } else if (pd.newPrice < pd.currentPrice && pd.newPrice < pd.targetPrice) { + pd.lutData.lowPriceJ = scaledReserves[j] * 1e18 / parityReserve; + pd.lutData.lowPriceI = scaledReserves[i] * 1e18 / parityReserve; + pd.lutData.lowPrice = pd.newPrice; + } + + // update max step size based on new scaled reserve. + pd.maxStepSize = scaledReserves[j] * (pd.lutData.lowPriceJ - pd.lutData.highPriceJ) / pd.lutData.lowPriceJ; + + pd.currentPrice = pd.newPrice; // check if new price is within 1 of target price: if (pd.currentPrice > pd.targetPrice) { @@ -288,7 +307,23 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { for (uint256 k; k < 255; k++) { scaledReserves[j] = updateReserve(pd, scaledReserves[j]); // calculate new price from reserves: - pd.currentPrice = calcRate(scaledReserves, i, j, abi.encode(18, 18)); + pd.newPrice = calcRate(scaledReserves, i, j, abi.encode(18, 18)); + + // if the new current price is either lower or higher than both the previous current price and the target price, + // (i.e the target price lies between the current price and the previous current price), + // recalibrate high/lowPrice and continue. + if (pd.newPrice > pd.targetPrice && pd.targetPrice > pd.currentPrice) { + pd.lutData.highPriceJ = scaledReserves[j] * 1e18 / scaledReserves[i]; + pd.lutData.highPrice = pd.newPrice; + } else if (pd.newPrice < pd.targetPrice && pd.targetPrice < pd.currentPrice) { + pd.lutData.lowPriceJ = scaledReserves[j] * 1e18 / scaledReserves[i]; + pd.lutData.lowPrice = pd.newPrice; + } + + // update max step size based on new scaled reserve. + pd.maxStepSize = scaledReserves[j] * (pd.lutData.lowPriceJ - pd.lutData.highPriceJ) / pd.lutData.lowPriceJ; + + pd.currentPrice = pd.newPrice; // check if new price is within PRICE_THRESHOLD: if (pd.currentPrice > pd.targetPrice) { diff --git a/test/TestHelper.sol b/test/TestHelper.sol index 3289be13..439b250d 100644 --- a/test/TestHelper.sol +++ b/test/TestHelper.sol @@ -13,6 +13,7 @@ import {Users} from "test/helpers/Users.sol"; import {Well, Call, IERC20, IWell, IWellFunction} from "src/Well.sol"; import {Aquifer} from "src/Aquifer.sol"; import {ConstantProduct2} from "src/functions/ConstantProduct2.sol"; +import {ConstantProduct} from "src/functions/ConstantProduct.sol"; import {Stable2} from "src/functions/Stable2.sol"; import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; @@ -75,6 +76,13 @@ abstract contract TestHelper is Test, WellDeployer { setupWell(n, deployWellFunction(), deployPumps(1)); } + function setup3Well() internal { + Call memory _wellFunction; + _wellFunction.target = address(new ConstantProduct()); + _wellFunction.data = new bytes(0); + setupWell(3, deployWellFunction(), deployPumps(1)); + } + function setupWell(uint256 n, Call[] memory _pumps) internal { setupWell(n, deployWellFunction(), _pumps); } diff --git a/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol index a229a992..83241aea 100644 --- a/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol +++ b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol @@ -111,7 +111,7 @@ contract BeanstalkStable2LiquidityTest is TestHelper { // estimated price and actual price are within 0.04% in the worst case. assertApproxEqRel(reservePrice0, targetPrice, 0.0004e18, "reservePrice0 <> targetPrice"); assertApproxEqRel(reservePrice1, targetPrice, 0.0004e18, "reservePrice1 <> targetPrice"); - assertApproxEqRel(reservePrice0, reservePrice1, 0.0004e18, "reservePrice0 <> reservePrice1"); + assertApproxEqRel(reservePrice0, reservePrice1, 0.0005e18, "reservePrice0 <> reservePrice1"); } } @@ -121,4 +121,20 @@ contract BeanstalkStable2LiquidityTest is TestHelper { vm.expectRevert(); _f.calcReserveAtRatioLiquidity(reserves, 2, ratios, ""); } + + function test_calcReserveAtRatioLiquidityExtreme() public view { + uint256[] memory reserves = new uint256[](2); + reserves[0] = 1e18; + reserves[1] = 1e18; + uint256[] memory ratios = new uint256[](2); + ratios[0] = 8; + ratios[1] = 1; + + uint256 reserve0 = _f.calcReserveAtRatioLiquidity(reserves, 0, ratios, data); + + reserves[0] = reserve0; + + uint256 price = _f.calcRate(reserves, 1, 0, data); + assertApproxEqRel(price, 125_000, 0.01e18); + } } diff --git a/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol index 7788e81f..36a95493 100644 --- a/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol +++ b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol @@ -60,8 +60,8 @@ contract BeanstalkStable2SwapTest is TestHelper { uint256 reserve0 = _f.calcReserveAtRatioSwap(reserves, 0, ratios, data); uint256 reserve1 = _f.calcReserveAtRatioSwap(reserves, 1, ratios, data); - assertEq(reserve0, 180.643950056605911775e18); // 180.64235400499155996e18, 100e18 - assertEq(reserve1, 39.474875366590812867e18); // 100e18, 39.474875366590812867e18 + assertEq(reserve0, 180.644064978044534737e18); // 180.644064978044534737e18, 100e18 + assertEq(reserve1, 39.475055811844664131e18); // 100e18, 39.475055811844664131e18 } function test_calcReserveAtRatioSwap_diff_diff() public view { @@ -101,4 +101,26 @@ contract BeanstalkStable2SwapTest is TestHelper { assertApproxEqRel(reservePrice0, targetPrice, 0.00015e18, "reservePrice0 <> targetPrice"); } } + + /** + * @notice verifies calcReserveAtRatioSwapExtreme works in the extreme ranges. + */ + function test_calcReserveAtRatioSwapExtreme() public view { + uint256[] memory reserves = new uint256[](2); + reserves[0] = 1e18; + reserves[1] = 1e18; + uint256[] memory ratios = new uint256[](2); + ratios[0] = 4202; + ratios[1] = 19_811; + uint256 targetPrice = uint256(ratios[0] * 1e6 / ratios[1]); + + uint256 reserve0 = _f.calcReserveAtRatioSwap(reserves, 0, ratios, data); + uint256 reserve1 = _f.calcReserveAtRatioSwap(reserves, 1, ratios, data); + + reserves[0] = reserve0; + reserves[1] = reserve1; + + uint256 price = _f.calcRate(reserves, 0, 1, data); + assertApproxEqAbs(price, targetPrice, 1); + } } From abf09ab7fc2b1e03f07e5f18951e1a3c0f39b4f7 Mon Sep 17 00:00:00 2001 From: Brean0 Date: Tue, 13 Aug 2024 17:46:02 +0200 Subject: [PATCH 02/22] remove unused test helper function. --- test/TestHelper.sol | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/TestHelper.sol b/test/TestHelper.sol index 439b250d..3289be13 100644 --- a/test/TestHelper.sol +++ b/test/TestHelper.sol @@ -13,7 +13,6 @@ import {Users} from "test/helpers/Users.sol"; import {Well, Call, IERC20, IWell, IWellFunction} from "src/Well.sol"; import {Aquifer} from "src/Aquifer.sol"; import {ConstantProduct2} from "src/functions/ConstantProduct2.sol"; -import {ConstantProduct} from "src/functions/ConstantProduct.sol"; import {Stable2} from "src/functions/Stable2.sol"; import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; @@ -76,13 +75,6 @@ abstract contract TestHelper is Test, WellDeployer { setupWell(n, deployWellFunction(), deployPumps(1)); } - function setup3Well() internal { - Call memory _wellFunction; - _wellFunction.target = address(new ConstantProduct()); - _wellFunction.data = new bytes(0); - setupWell(3, deployWellFunction(), deployPumps(1)); - } - function setupWell(uint256 n, Call[] memory _pumps) internal { setupWell(n, deployWellFunction(), _pumps); } From 4eca74c1e15e08e15b3d30a44f82368fb8df53f0 Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 14 Aug 2024 11:02:59 +0300 Subject: [PATCH 03/22] Add access control to authorizeUpgrade --- src/WellUpgradeable.sol | 2 +- test/WellUpgradeable.t.sol | 92 ++++++++++++++++++++++++++++++++------ 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/src/WellUpgradeable.sol b/src/WellUpgradeable.sol index ea60c50d..6ae3c712 100644 --- a/src/WellUpgradeable.sol +++ b/src/WellUpgradeable.sol @@ -62,7 +62,7 @@ contract WellUpgradeable is Well, UUPSUpgradeable, OwnableUpgradeable { * @notice Check that the execution is being performed through a delegatecall call and that the execution context is * a proxy contract with an ERC1167 minimal proxy from an aquifier, pointing to a well implmentation. */ - function _authorizeUpgrade(address newImplmentation) internal view override { + function _authorizeUpgrade(address newImplmentation) internal view override onlyOwner { // verify the function is called through a delegatecall. require(address(this) != ___self, "Function must be called through delegatecall"); diff --git a/test/WellUpgradeable.t.sol b/test/WellUpgradeable.t.sol index a14b291d..01d99b99 100644 --- a/test/WellUpgradeable.t.sol +++ b/test/WellUpgradeable.t.sol @@ -49,7 +49,10 @@ contract WellUpgradeTest is Test, WellDeployer { IWellFunction cp2 = new ConstantProduct2(); vm.label(address(cp2), "CP2"); wellFunctionAddress = address(cp2); - Call memory wellFunction = Call(address(cp2), abi.encode("beanstalkFunction")); + Call memory wellFunction = Call( + address(cp2), + abi.encode("beanstalkFunction") + ); // Pump IPump mockPump = new MockPump(); @@ -65,8 +68,14 @@ contract WellUpgradeTest is Test, WellDeployer { initialOwner = makeAddr("owner"); // Well - WellUpgradeable well = - encodeAndBoreWellUpgradeable(aquifer, wellImplementation, tokens, wellFunction, pumps, bytes32(0)); + WellUpgradeable well = encodeAndBoreWellUpgradeable( + aquifer, + wellImplementation, + tokens, + wellFunction, + pumps, + bytes32(0) + ); wellAddress = address(well); vm.label(wellAddress, "upgradeableWell"); // Sum up of what is going on here @@ -92,7 +101,10 @@ contract WellUpgradeTest is Test, WellDeployer { vm.startPrank(initialOwner); ERC1967Proxy proxy = new ERC1967Proxy( address(well), // implementation address - LibWellUpgradeableConstructor.encodeWellInitFunctionCall(tokens, wellFunction) // init data + LibWellUpgradeableConstructor.encodeWellInitFunctionCall( + tokens, + wellFunction + ) // init data ); vm.stopPrank(); proxyAddress = address(proxy); @@ -127,8 +139,12 @@ contract WellUpgradeTest is Test, WellDeployer { } function testProxyGetWellFunction() public { - Call memory proxyWellFunction = WellUpgradeable(proxyAddress).wellFunction(); - assertEq(address(proxyWellFunction.target), address(wellFunctionAddress)); + Call memory proxyWellFunction = WellUpgradeable(proxyAddress) + .wellFunction(); + assertEq( + address(proxyWellFunction.target), + address(wellFunctionAddress) + ); assertEq(proxyWellFunction.data, abi.encode("beanstalkFunction")); } @@ -143,7 +159,10 @@ contract WellUpgradeTest is Test, WellDeployer { function testProxyNumTokens() public { uint256 expectedNumTokens = 2; - assertEq(expectedNumTokens, WellUpgradeable(proxyAddress).numberOfTokens()); + assertEq( + expectedNumTokens, + WellUpgradeable(proxyAddress).numberOfTokens() + ); } ///////////////// Interaction test ////////////////// @@ -153,8 +172,18 @@ contract WellUpgradeTest is Test, WellDeployer { uint256[] memory amounts = new uint256[](2); amounts[0] = 1000; amounts[1] = 1000; - WellUpgradeable(wellAddress).addLiquidity(amounts, 0, user, type(uint256).max); - WellUpgradeable(proxyAddress).addLiquidity(amounts, 0, user, type(uint256).max); + WellUpgradeable(wellAddress).addLiquidity( + amounts, + 0, + user, + type(uint256).max + ); + WellUpgradeable(proxyAddress).addLiquidity( + amounts, + 0, + user, + type(uint256).max + ); assertEq(amounts, WellUpgradeable(proxyAddress).getReserves()); vm.stopPrank(); } @@ -173,9 +202,9 @@ contract WellUpgradeTest is Test, WellDeployer { } function testRevertTransferOwnershipFromNotOnwer() public { - vm.expectRevert(); address notOwner = makeAddr("notOwner"); vm.prank(notOwner); + vm.expectRevert(); WellUpgradeable(proxyAddress).transferOwnership(notOwner); } @@ -190,8 +219,14 @@ contract WellUpgradeTest is Test, WellDeployer { pumps[0] = Call(mockPumpAddress, abi.encode("2")); // create new mock Well Implementation: address wellImpl = address(new MockWellUpgradeable()); - WellUpgradeable well2 = - encodeAndBoreWellUpgradeable(aquifer, wellImpl, tokens, wellFunction, pumps, bytes32(abi.encode("2"))); + WellUpgradeable well2 = encodeAndBoreWellUpgradeable( + aquifer, + wellImpl, + tokens, + wellFunction, + pumps, + bytes32(abi.encode("2")) + ); vm.label(address(well2), "upgradeableWell2"); vm.startPrank(initialOwner); @@ -199,9 +234,40 @@ contract WellUpgradeTest is Test, WellDeployer { proxy.upgradeTo(address(well2)); assertEq(initialOwner, MockWellUpgradeable(proxyAddress).owner()); // verify proxy was upgraded. - assertEq(address(well2), MockWellUpgradeable(proxyAddress).getImplementation()); + assertEq( + address(well2), + MockWellUpgradeable(proxyAddress).getImplementation() + ); assertEq(1, MockWellUpgradeable(proxyAddress).getVersion()); assertEq(100, MockWellUpgradeable(proxyAddress).getVersion(100)); vm.stopPrank(); } + + function testUpgradeToNewImplementationAccessControl() public { + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = new MockToken("BEAN", "BEAN", 6); + tokens[1] = new MockToken("WETH", "WETH", 18); + Call memory wellFunction = Call(wellFunctionAddress, abi.encode("2")); + Call[] memory pumps = new Call[](1); + pumps[0] = Call(mockPumpAddress, abi.encode("2")); + // create new mock Well Implementation: + address wellImpl = address(new MockWellUpgradeable()); + WellUpgradeable well2 = encodeAndBoreWellUpgradeable( + aquifer, + wellImpl, + tokens, + wellFunction, + pumps, + bytes32(abi.encode("2")) + ); + vm.label(address(well2), "upgradeableWell2"); + // set caller to not be the owner + address notOwner = makeAddr("notOwner"); + vm.startPrank(notOwner); + WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress)); + // expect revert + vm.expectRevert(); + proxy.upgradeTo(address(well2)); + vm.stopPrank(); + } } From 07e9924c64c5049c116bc3d5e1092630cbf4e17a Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 14 Aug 2024 11:22:13 +0300 Subject: [PATCH 04/22] Add explicit revert statements in the case of non convergence --- src/functions/Stable2.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/functions/Stable2.sol b/src/functions/Stable2.sol index ca25edc0..2514db25 100644 --- a/src/functions/Stable2.sol +++ b/src/functions/Stable2.sol @@ -100,6 +100,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { if (prevReserves - lpTokenSupply <= 1) return lpTokenSupply; } } + revert("Non convergence: calcLpTokenSupply"); } /** @@ -140,7 +141,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { } } } - revert("did not find convergence"); + revert("Non convergence: calcReserve"); } /** @@ -236,6 +237,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { } } } + revert("Non convergence: calcReserveAtRatioSwap"); } /** @@ -301,6 +303,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { } } } + revert("Non convergence: calcReserveAtRatioLiquidity"); } /** From 82a9f3ce5ada2c0f006441fdf27fb3a165af83ed Mon Sep 17 00:00:00 2001 From: Brean0 Date: Wed, 14 Aug 2024 10:53:24 +0200 Subject: [PATCH 05/22] impose limits on reserves, update formmating --- src/Well.sol | 27 +++++++++----------------- src/interfaces/IWell.sol | 7 +++---- src/libraries/LibLastReserveBytes.sol | 8 +++----- test/LiquidityHelper.sol | 14 ++++++------- test/functions/Stable2.t.sol | 11 +++++++---- test/integration/interfaces/ICurve.sol | 2 +- 6 files changed, 29 insertions(+), 40 deletions(-) diff --git a/src/Well.sol b/src/Well.sol index 84be74a8..f14793f4 100644 --- a/src/Well.sol +++ b/src/Well.sol @@ -484,12 +484,9 @@ contract Well is ERC20PermitUpgradeable, IWell, IWellErrors, ReentrancyGuardUpgr /** * @dev Assumes that no tokens involved incur a fee on transfer. */ - function getAddLiquidityOut(uint256[] memory tokenAmountsIn) - external - view - readOnlyNonReentrant - returns (uint256 lpAmountOut) - { + function getAddLiquidityOut( + uint256[] memory tokenAmountsIn + ) external view readOnlyNonReentrant returns (uint256 lpAmountOut) { IERC20[] memory _tokens = tokens(); uint256 tokensLength = _tokens.length; uint256[] memory reserves = _getReserves(tokensLength); @@ -527,12 +524,9 @@ contract Well is ERC20PermitUpgradeable, IWell, IWellErrors, ReentrancyGuardUpgr emit RemoveLiquidity(lpAmountIn, tokenAmountsOut, recipient); } - function getRemoveLiquidityOut(uint256 lpAmountIn) - external - view - readOnlyNonReentrant - returns (uint256[] memory tokenAmountsOut) - { + function getRemoveLiquidityOut( + uint256 lpAmountIn + ) external view readOnlyNonReentrant returns (uint256[] memory tokenAmountsOut) { IERC20[] memory _tokens = tokens(); uint256[] memory reserves = _getReserves(_tokens.length); uint256 lpTokenSupply = totalSupply(); @@ -620,12 +614,9 @@ contract Well is ERC20PermitUpgradeable, IWell, IWellErrors, ReentrancyGuardUpgr emit RemoveLiquidity(lpAmountIn, tokenAmountsOut, recipient); } - function getRemoveLiquidityImbalancedIn(uint256[] calldata tokenAmountsOut) - external - view - readOnlyNonReentrant - returns (uint256 lpAmountIn) - { + function getRemoveLiquidityImbalancedIn( + uint256[] calldata tokenAmountsOut + ) external view readOnlyNonReentrant returns (uint256 lpAmountIn) { IERC20[] memory _tokens = tokens(); uint256 tokensLength = _tokens.length; uint256[] memory reserves = _getReserves(tokensLength); diff --git a/src/interfaces/IWell.sol b/src/interfaces/IWell.sol index f7b00c47..1b734108 100644 --- a/src/interfaces/IWell.sol +++ b/src/interfaces/IWell.sol @@ -357,10 +357,9 @@ interface IWell { * @param tokenAmountsOut The amount of each underlying token to receive; MUST match the indexing of {Well.tokens} * @return lpAmountIn The amount of LP tokens burned */ - function getRemoveLiquidityImbalancedIn(uint256[] calldata tokenAmountsOut) - external - view - returns (uint256 lpAmountIn); + function getRemoveLiquidityImbalancedIn( + uint256[] calldata tokenAmountsOut + ) external view returns (uint256 lpAmountIn); //////////////////// RESERVES //////////////////// diff --git a/src/libraries/LibLastReserveBytes.sol b/src/libraries/LibLastReserveBytes.sol index 89aa9d1b..cfd43a34 100644 --- a/src/libraries/LibLastReserveBytes.sol +++ b/src/libraries/LibLastReserveBytes.sol @@ -82,11 +82,9 @@ library LibLastReserveBytes { /** * @dev Read `n` packed bytes16 reserves at storage position `slot`. */ - function readLastReserves(bytes32 slot) - internal - view - returns (uint8 n, uint40 lastTimestamp, uint256[] memory lastReserves) - { + function readLastReserves( + bytes32 slot + ) internal view returns (uint8 n, uint40 lastTimestamp, uint256[] memory lastReserves) { // Shortcut: two reserves can be quickly unpacked from one slot bytes32 temp; assembly { diff --git a/test/LiquidityHelper.sol b/test/LiquidityHelper.sol index 4f9164ba..37d047ce 100644 --- a/test/LiquidityHelper.sol +++ b/test/LiquidityHelper.sol @@ -48,10 +48,9 @@ contract LiquidityHelper is TestHelper { return beforeAddLiquidity(action); } - function beforeAddLiquidity(AddLiquidityAction memory action) - internal - returns (Snapshot memory, AddLiquidityAction memory) - { + function beforeAddLiquidity( + AddLiquidityAction memory action + ) internal returns (Snapshot memory, AddLiquidityAction memory) { Snapshot memory beforeSnapshot = _newSnapshot(); uint256[] memory amountToTransfer = new uint256[](tokens.length); @@ -97,10 +96,9 @@ contract LiquidityHelper is TestHelper { return beforeRemoveLiquidity(action); } - function beforeRemoveLiquidity(RemoveLiquidityAction memory action) - internal - returns (Snapshot memory, RemoveLiquidityAction memory) - { + function beforeRemoveLiquidity( + RemoveLiquidityAction memory action + ) internal returns (Snapshot memory, RemoveLiquidityAction memory) { Snapshot memory beforeSnapshot = _newSnapshot(); vm.expectEmit(true, true, true, true); diff --git a/test/functions/Stable2.t.sol b/test/functions/Stable2.t.sol index 3cff992b..b9385917 100644 --- a/test/functions/Stable2.t.sol +++ b/test/functions/Stable2.t.sol @@ -126,7 +126,9 @@ contract Stable2Test is WellFunctionHelper { _data = abi.encode(18, 18); uint256[] memory reserves = new uint256[](2); reserves[0] = bound(_reserves[0], 10e18, MAX_RESERVE); - reserves[1] = bound(_reserves[1], 10e18, MAX_RESERVE); + // reserve 1 must be at least 1/800th of the value of reserves[0]. + uint256 reserve1MinValue = (reserves[0] / 8e2) < 10e18 ? 10e18 : reserves[0] / 8e2; + reserves[1] = bound(_reserves[1], reserve1MinValue, MAX_RESERVE); uint256 lpTokenSupply = _function.calcLpTokenSupply(reserves, _data); uint256[] memory underlying = _function.calcLPTokenUnderlying(lpTokenSupply, reserves, lpTokenSupply, _data); @@ -137,11 +139,12 @@ contract Stable2Test is WellFunctionHelper { //////////// FUZZ //////////// - function testFuzz_stableSwap(uint256 x, uint256 y, uint256 a) public { + function testFuzz_stableSwap(uint256 x, uint256 y) public { uint256[] memory reserves = new uint256[](2); reserves[0] = bound(x, 10e18, MAX_RESERVE); - reserves[1] = bound(y, 10e18, MAX_RESERVE); - a = bound(a, 1, 1_000_000); + // reserve 1 must be at least 1/800th of the value of reserves[0]. + uint256 reserve1MinValue = (reserves[0] / 8e2) < 10e18 ? 10e18 : reserves[0] / 8e2; + reserves[1] = bound(y, reserve1MinValue, MAX_RESERVE); _data = abi.encode(18, 18); diff --git a/test/integration/interfaces/ICurve.sol b/test/integration/interfaces/ICurve.sol index 3a0535f3..ef0850b6 100644 --- a/test/integration/interfaces/ICurve.sol +++ b/test/integration/interfaces/ICurve.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT pragma experimental ABIEncoderV2; -pragma solidity 0.8.20; +pragma solidity ^0.8.20; interface ICurvePool { function A_precise() external view returns (uint256); From af35125e4398eb27b93c5fb843a2d2f67e7ad1ac Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 14 Aug 2024 12:02:15 +0300 Subject: [PATCH 06/22] Add check for well tokens in authorizeUpgrade --- src/WellUpgradeable.sol | 8 ++++++ test/WellUpgradeable.t.sol | 57 +++++++++++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/WellUpgradeable.sol b/src/WellUpgradeable.sol index ea60c50d..6bca00c1 100644 --- a/src/WellUpgradeable.sol +++ b/src/WellUpgradeable.sol @@ -77,6 +77,14 @@ contract WellUpgradeable is Well, UUPSUpgradeable, OwnableUpgradeable { "New implementation must be a well implmentation" ); + // verify the new well uses the same tokens in the same order. + IERC20[] memory _tokens = tokens(); + IERC20[] memory newTokens = WellUpgradeable(newImplmentation).tokens(); + require(_tokens.length == newTokens.length, "New well must use the same number of tokens"); + for (uint256 i; i < _tokens.length; ++i) { + require(_tokens[i] == newTokens[i], "New well must use the same tokens in the same order"); + } + // verify the new implmentation is a valid ERC-1967 implmentation. require( UUPSUpgradeable(newImplmentation).proxiableUUID() == _IMPLEMENTATION_SLOT, diff --git a/test/WellUpgradeable.t.sol b/test/WellUpgradeable.t.sol index a14b291d..76f44bfe 100644 --- a/test/WellUpgradeable.t.sol +++ b/test/WellUpgradeable.t.sol @@ -28,12 +28,14 @@ contract WellUpgradeTest is Test, WellDeployer { address token2Address; address wellAddress; address wellImplementation; + IERC20[] tokens = new IERC20[](2); function setUp() public { // Tokens - IERC20[] memory tokens = new IERC20[](2); - tokens[0] = new MockToken("BEAN", "BEAN", 6); - tokens[1] = new MockToken("WETH", "WETH", 18); + IERC20 token0 = new MockToken("BEAN", "BEAN", 6); + IERC20 token1 = new MockToken("WETH", "WETH", 18); + tokens[0] = token0; + tokens[1] = token1; token1Address = address(tokens[0]); vm.label(token1Address, "token1"); @@ -182,18 +184,15 @@ contract WellUpgradeTest is Test, WellDeployer { ////////////////////// Upgrade Tests ////////////////////// function testUpgradeToNewImplementation() public { - IERC20[] memory tokens = new IERC20[](2); - tokens[0] = new MockToken("BEAN", "BEAN", 6); - tokens[1] = new MockToken("WETH", "WETH", 18); Call memory wellFunction = Call(wellFunctionAddress, abi.encode("2")); Call[] memory pumps = new Call[](1); pumps[0] = Call(mockPumpAddress, abi.encode("2")); // create new mock Well Implementation: address wellImpl = address(new MockWellUpgradeable()); + // bore new well with the same 2 tokens WellUpgradeable well2 = encodeAndBoreWellUpgradeable(aquifer, wellImpl, tokens, wellFunction, pumps, bytes32(abi.encode("2"))); vm.label(address(well2), "upgradeableWell2"); - vm.startPrank(initialOwner); WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress)); proxy.upgradeTo(address(well2)); @@ -204,4 +203,48 @@ contract WellUpgradeTest is Test, WellDeployer { assertEq(100, MockWellUpgradeable(proxyAddress).getVersion(100)); vm.stopPrank(); } + + function testUpgradeToNewImplementationDiffTokens() public { + // create 2 new tokens with new addresses + IERC20[] memory newTokens = new IERC20[](2); + newTokens[0] = new MockToken("WBTC", "WBTC", 6); + newTokens[1] = new MockToken("WETH2", "WETH2", 18); + Call memory wellFunction = Call(wellFunctionAddress, abi.encode("2")); + Call[] memory pumps = new Call[](1); + pumps[0] = Call(mockPumpAddress, abi.encode("2")); + // create new mock Well Implementation: + address wellImpl = address(new MockWellUpgradeable()); + // bore new well with the different tokens + WellUpgradeable well2 = + encodeAndBoreWellUpgradeable(aquifer, wellImpl, newTokens, wellFunction, pumps, bytes32(abi.encode("2"))); + vm.label(address(well2), "upgradeableWell2"); + vm.startPrank(initialOwner); + WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress)); + // expect revert since new well uses different tokens + vm.expectRevert("New well must use the same tokens in the same order"); + proxy.upgradeTo(address(well2)); + vm.stopPrank(); + } + + function testUpgradeToNewImplementationDiffTokenOrder() public { + // create 2 new tokens with new addresses + IERC20[] memory newTokens = new IERC20[](2); + newTokens[0] = tokens[1]; + newTokens[1] = tokens[0]; + Call memory wellFunction = Call(wellFunctionAddress, abi.encode("2")); + Call[] memory pumps = new Call[](1); + pumps[0] = Call(mockPumpAddress, abi.encode("2")); + // create new mock Well Implementation: + address wellImpl = address(new MockWellUpgradeable()); + // bore new well with the different tokens + WellUpgradeable well2 = + encodeAndBoreWellUpgradeable(aquifer, wellImpl, newTokens, wellFunction, pumps, bytes32(abi.encode("2"))); + vm.label(address(well2), "upgradeableWell2"); + vm.startPrank(initialOwner); + WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress)); + // expect revert since new well uses different tokens + vm.expectRevert("New well must use the same tokens in the same order"); + proxy.upgradeTo(address(well2)); + vm.stopPrank(); + } } From 0143ccf549b441962f459d370ed889b45c49e7d0 Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 14 Aug 2024 12:25:31 +0300 Subject: [PATCH 07/22] decimal fix --- src/functions/Stable2.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/Stable2.sol b/src/functions/Stable2.sol index ca25edc0..bd5fed7b 100644 --- a/src/functions/Stable2.sol +++ b/src/functions/Stable2.sol @@ -314,7 +314,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { if (decimal0 == 0) { decimal0 = 18; } - if (decimal0 == 0) { + if (decimal1 == 0) { decimal1 = 18; } if (decimal0 > 18 || decimal1 > 18) revert InvalidTokenDecimals(); From 812bca2ddaf1fe7928e1059cc9ebf7fa1a738942 Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 14 Aug 2024 12:48:58 +0300 Subject: [PATCH 08/22] remove else statment in notDelegatedOrIsMinimalProxy modifier --- src/WellUpgradeable.sol | 4 +--- test/WellUpgradeable.t.sol | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/WellUpgradeable.sol b/src/WellUpgradeable.sol index ea60c50d..91c6d591 100644 --- a/src/WellUpgradeable.sol +++ b/src/WellUpgradeable.sol @@ -17,15 +17,13 @@ contract WellUpgradeable is Well, UUPSUpgradeable, OwnableUpgradeable { address private immutable ___self = address(this); /** - * @notice verifies that the execution is called through an minimal proxy or is not a delegate call. + * @notice Verifies that the execution is called through an minimal proxy. */ modifier notDelegatedOrIsMinimalProxy() { if (address(this) != ___self) { address aquifer = aquifer(); address wellImplmentation = IAquifer(aquifer).wellImplementation(address(this)); require(wellImplmentation == ___self, "Function must be called by a Well bored by an aquifer"); - } else { - revert("UUPSUpgradeable: must not be called through delegatecall"); } _; } diff --git a/test/WellUpgradeable.t.sol b/test/WellUpgradeable.t.sol index a14b291d..deeb04d1 100644 --- a/test/WellUpgradeable.t.sol +++ b/test/WellUpgradeable.t.sol @@ -74,7 +74,7 @@ contract WellUpgradeTest is Test, WellDeployer { // The well upgradeable additionally takes in an owner address so we modify the init function call // to include the owner address. // When the new well is deployed, all init data are stored in the implementation storage - // including pump and well function data --> NOTE: This could be an issue but how do we solve this? + // including pump and well function data // Then we deploy a ERC1967Proxy proxy for the well upgradeable and call the init function on the proxy // When we deploy the proxy, the init data is stored in the proxy storage and the well is initialized // for the second time. We can now control the well via delegate calls to the proxy address. From 453d388ff1379dd99e55d82780e9116b42b798fe Mon Sep 17 00:00:00 2001 From: Brean0 Date: Wed, 14 Aug 2024 12:02:10 +0200 Subject: [PATCH 09/22] update tests --- test/Stable2/Well.Stable2.AddLiquidity.t.sol | 4 +++- .../BeanstalkStable2.calcReserveAtRatioSwap.t.sol | 8 +++++++- test/functions/Stable2.t.sol | 8 ++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/test/Stable2/Well.Stable2.AddLiquidity.t.sol b/test/Stable2/Well.Stable2.AddLiquidity.t.sol index 710e6e9f..282bf692 100644 --- a/test/Stable2/Well.Stable2.AddLiquidity.t.sol +++ b/test/Stable2/Well.Stable2.AddLiquidity.t.sol @@ -158,7 +158,9 @@ contract WellStable2AddLiquidityTest is LiquidityHelper { // amounts to add as liquidity uint256[] memory amounts = new uint256[](2); amounts[0] = bound(x, 0, type(uint104).max); - amounts[1] = bound(y, 0, type(uint104).max); + // reserve 1 must be at least 1/600th of the value of amounts[0]. + uint256 reserve1MinValue = (amounts[0] / 6e2) < 10e18 ? 10e18 : amounts[0] / 6e2; + amounts[1] = bound(y, reserve1MinValue, type(uint104).max); mintTokens(user, amounts); Snapshot memory before; diff --git a/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol index 7788e81f..4a70f807 100644 --- a/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol +++ b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol @@ -83,7 +83,13 @@ contract BeanstalkStable2SwapTest is TestHelper { for (uint256 i; i < 2; ++i) { // Upper bound is limited by stableSwap, // due to the stableswap reserves being extremely far apart. - reserves[i] = bound(reserves[i], 1e18, 1e31); + + if (i == 1) { + uint256 reserve1MinValue = (reserves[0] / 6e2) < 10e18 ? 10e18 : reserves[0] / 6e2; + reserves[1] = bound(reserves[i], reserve1MinValue, 1e31); + } else { + reserves[i] = bound(reserves[i], 1e18, 1e31); + } ratios[i] = bound(ratios[i], 1e18, 4e18); } diff --git a/test/functions/Stable2.t.sol b/test/functions/Stable2.t.sol index b9385917..eeb9c731 100644 --- a/test/functions/Stable2.t.sol +++ b/test/functions/Stable2.t.sol @@ -126,8 +126,8 @@ contract Stable2Test is WellFunctionHelper { _data = abi.encode(18, 18); uint256[] memory reserves = new uint256[](2); reserves[0] = bound(_reserves[0], 10e18, MAX_RESERVE); - // reserve 1 must be at least 1/800th of the value of reserves[0]. - uint256 reserve1MinValue = (reserves[0] / 8e2) < 10e18 ? 10e18 : reserves[0] / 8e2; + // reserve 1 must be at least 1/600th of the value of reserves[0]. + uint256 reserve1MinValue = (reserves[0] / 6e2) < 10e18 ? 10e18 : reserves[0] / 6e2; reserves[1] = bound(_reserves[1], reserve1MinValue, MAX_RESERVE); uint256 lpTokenSupply = _function.calcLpTokenSupply(reserves, _data); @@ -142,8 +142,8 @@ contract Stable2Test is WellFunctionHelper { function testFuzz_stableSwap(uint256 x, uint256 y) public { uint256[] memory reserves = new uint256[](2); reserves[0] = bound(x, 10e18, MAX_RESERVE); - // reserve 1 must be at least 1/800th of the value of reserves[0]. - uint256 reserve1MinValue = (reserves[0] / 8e2) < 10e18 ? 10e18 : reserves[0] / 8e2; + // reserve 1 must be at least 1/600th of the value of reserves[0]. + uint256 reserve1MinValue = (reserves[0] / 6e2) < 10e18 ? 10e18 : reserves[0] / 6e2; reserves[1] = bound(y, reserve1MinValue, MAX_RESERVE); _data = abi.encode(18, 18); From 54ac748e50d22491ab18e9d2259ea97c58d09621 Mon Sep 17 00:00:00 2001 From: Brean0 Date: Wed, 14 Aug 2024 12:08:08 +0200 Subject: [PATCH 10/22] update optimizer to compile. --- foundry.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index fc84913c..e8ae0e3a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ out = 'out' libs = ['lib', 'node_modules'] fuzz = { runs = 256 } optimizer = true -optimizer_runs = 400 +optimizer_runs = 200 remappings = [ '@openzeppelin/=node_modules/@openzeppelin/', ] @@ -27,7 +27,6 @@ ignore = ["src/libraries/LibClone.sol", "src/utils/Clone.sol", "src/libraries/AB int_types = "long" line_length = 120 multiline_func_header = "params_first" -number_underscore = "thousands" override_spacing = false quote_style = "double" tab_width = 4 From 1409811c8dd74001c1e346123ff6e5ad7858aed7 Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 21 Aug 2024 13:51:30 +0300 Subject: [PATCH 11/22] fix tests --- test/WellUpgradeable.t.sol | 48 +++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/test/WellUpgradeable.t.sol b/test/WellUpgradeable.t.sol index 1b3a8cea..61ca430e 100644 --- a/test/WellUpgradeable.t.sol +++ b/test/WellUpgradeable.t.sol @@ -241,15 +241,9 @@ contract WellUpgradeTest is Test, WellDeployer { vm.stopPrank(); } + ///////////////// Access Control //////////////////// + function testUpgradeToNewImplementationAccessControl() public { - IERC20[] memory tokens = new IERC20[](2); - tokens[0] = new MockToken("BEAN", "BEAN", 6); - tokens[1] = new MockToken("WETH", "WETH", 18); - function testUpgradeToNewImplementationDiffTokens() public { - // create 2 new tokens with new addresses - IERC20[] memory newTokens = new IERC20[](2); - newTokens[0] = new MockToken("WBTC", "WBTC", 6); - newTokens[1] = new MockToken("WETH2", "WETH2", 18); Call memory wellFunction = Call(wellFunctionAddress, abi.encode("2")); Call[] memory pumps = new Call[](1); pumps[0] = Call(mockPumpAddress, abi.encode("2")); @@ -269,10 +263,32 @@ contract WellUpgradeTest is Test, WellDeployer { vm.startPrank(notOwner); WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress)); // expect revert - vm.expectRevert(); + vm.expectRevert("Ownable: caller is not the owner"); + proxy.upgradeTo(address(well2)); + vm.stopPrank(); + } + + ///////////////////// Token Check ////////////////////// + + function testUpgradeToNewImplementationDiffTokens() public { + // create 2 new tokens with new addresses + IERC20[] memory newTokens = new IERC20[](2); + newTokens[0] = new MockToken("WBTC", "WBTC", 6); + newTokens[1] = new MockToken("WETH2", "WETH2", 18); + Call memory wellFunction = Call(wellFunctionAddress, abi.encode("2")); + Call[] memory pumps = new Call[](1); + pumps[0] = Call(mockPumpAddress, abi.encode("2")); + // create new mock Well Implementation: + address wellImpl = address(new MockWellUpgradeable()); // bore new well with the different tokens - WellUpgradeable well2 = - encodeAndBoreWellUpgradeable(aquifer, wellImpl, newTokens, wellFunction, pumps, bytes32(abi.encode("2"))); + WellUpgradeable well2 = encodeAndBoreWellUpgradeable( + aquifer, + wellImpl, + newTokens, + wellFunction, + pumps, + bytes32(abi.encode("2")) + ); vm.label(address(well2), "upgradeableWell2"); vm.startPrank(initialOwner); WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress)); @@ -293,8 +309,14 @@ contract WellUpgradeTest is Test, WellDeployer { // create new mock Well Implementation: address wellImpl = address(new MockWellUpgradeable()); // bore new well with the different tokens - WellUpgradeable well2 = - encodeAndBoreWellUpgradeable(aquifer, wellImpl, newTokens, wellFunction, pumps, bytes32(abi.encode("2"))); + WellUpgradeable well2 = encodeAndBoreWellUpgradeable( + aquifer, + wellImpl, + newTokens, + wellFunction, + pumps, + bytes32(abi.encode("2")) + ); vm.label(address(well2), "upgradeableWell2"); vm.startPrank(initialOwner); WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress)); From a9c1f68abda903293e5505b7b0ff258fca27556c Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 21 Aug 2024 14:05:03 +0300 Subject: [PATCH 12/22] update comment in well upgr test --- test/WellUpgradeable.t.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/WellUpgradeable.t.sol b/test/WellUpgradeable.t.sol index 61ca430e..3e3c35ae 100644 --- a/test/WellUpgradeable.t.sol +++ b/test/WellUpgradeable.t.sol @@ -90,9 +90,8 @@ contract WellUpgradeTest is Test, WellDeployer { // When we deploy the proxy, the init data is stored in the proxy storage and the well is initialized // for the second time. We can now control the well via delegate calls to the proxy address. - // Every time we call the init function, we init the owner to be the msg.sender and - // then immidiately transfer ownership - // to an address of our choice (see WellUpgradeable.sol for more details on the init function) + // Every time we call the init function, we init the owner to be the msg.sender + // (see WellUpgradeable.sol for more details on the init function) // FROM OZ // If _data is nonempty, it’s used as data in a delegate call to _logic. From 3241f9cc30f68221990b33af501c3fe8fea97968 Mon Sep 17 00:00:00 2001 From: Brean0 Date: Wed, 21 Aug 2024 15:33:12 +0200 Subject: [PATCH 13/22] update package version, formatting. --- package.json | 2 +- test/WellUpgradeable.t.sol | 92 ++++++++------------------------------ 2 files changed, 19 insertions(+), 75 deletions(-) diff --git a/package.json b/package.json index 27dc8b79..cc05a548 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@beanstalk/wells", - "version": "1.2.0-prerelease1", + "version": "1.2.0-prerelease2", "description": "A [{Well}](/src/Well.sol) is a constant function AMM that allows the provisioning of liquidity into a single pooled on-chain liquidity position.", "main": "index.js", "directories": { diff --git a/test/WellUpgradeable.t.sol b/test/WellUpgradeable.t.sol index 3e3c35ae..e94ee079 100644 --- a/test/WellUpgradeable.t.sol +++ b/test/WellUpgradeable.t.sol @@ -51,10 +51,7 @@ contract WellUpgradeTest is Test, WellDeployer { IWellFunction cp2 = new ConstantProduct2(); vm.label(address(cp2), "CP2"); wellFunctionAddress = address(cp2); - Call memory wellFunction = Call( - address(cp2), - abi.encode("beanstalkFunction") - ); + Call memory wellFunction = Call(address(cp2), abi.encode("beanstalkFunction")); // Pump IPump mockPump = new MockPump(); @@ -70,14 +67,8 @@ contract WellUpgradeTest is Test, WellDeployer { initialOwner = makeAddr("owner"); // Well - WellUpgradeable well = encodeAndBoreWellUpgradeable( - aquifer, - wellImplementation, - tokens, - wellFunction, - pumps, - bytes32(0) - ); + WellUpgradeable well = + encodeAndBoreWellUpgradeable(aquifer, wellImplementation, tokens, wellFunction, pumps, bytes32(0)); wellAddress = address(well); vm.label(wellAddress, "upgradeableWell"); // Sum up of what is going on here @@ -102,10 +93,7 @@ contract WellUpgradeTest is Test, WellDeployer { vm.startPrank(initialOwner); ERC1967Proxy proxy = new ERC1967Proxy( address(well), // implementation address - LibWellUpgradeableConstructor.encodeWellInitFunctionCall( - tokens, - wellFunction - ) // init data + LibWellUpgradeableConstructor.encodeWellInitFunctionCall(tokens, wellFunction) // init data ); vm.stopPrank(); proxyAddress = address(proxy); @@ -140,12 +128,8 @@ contract WellUpgradeTest is Test, WellDeployer { } function testProxyGetWellFunction() public { - Call memory proxyWellFunction = WellUpgradeable(proxyAddress) - .wellFunction(); - assertEq( - address(proxyWellFunction.target), - address(wellFunctionAddress) - ); + Call memory proxyWellFunction = WellUpgradeable(proxyAddress).wellFunction(); + assertEq(address(proxyWellFunction.target), address(wellFunctionAddress)); assertEq(proxyWellFunction.data, abi.encode("beanstalkFunction")); } @@ -160,10 +144,7 @@ contract WellUpgradeTest is Test, WellDeployer { function testProxyNumTokens() public { uint256 expectedNumTokens = 2; - assertEq( - expectedNumTokens, - WellUpgradeable(proxyAddress).numberOfTokens() - ); + assertEq(expectedNumTokens, WellUpgradeable(proxyAddress).numberOfTokens()); } ///////////////// Interaction test ////////////////// @@ -173,18 +154,8 @@ contract WellUpgradeTest is Test, WellDeployer { uint256[] memory amounts = new uint256[](2); amounts[0] = 1000; amounts[1] = 1000; - WellUpgradeable(wellAddress).addLiquidity( - amounts, - 0, - user, - type(uint256).max - ); - WellUpgradeable(proxyAddress).addLiquidity( - amounts, - 0, - user, - type(uint256).max - ); + WellUpgradeable(wellAddress).addLiquidity(amounts, 0, user, type(uint256).max); + WellUpgradeable(proxyAddress).addLiquidity(amounts, 0, user, type(uint256).max); assertEq(amounts, WellUpgradeable(proxyAddress).getReserves()); vm.stopPrank(); } @@ -217,24 +188,15 @@ contract WellUpgradeTest is Test, WellDeployer { pumps[0] = Call(mockPumpAddress, abi.encode("2")); // create new mock Well Implementation: address wellImpl = address(new MockWellUpgradeable()); - WellUpgradeable well2 = encodeAndBoreWellUpgradeable( - aquifer, - wellImpl, - tokens, - wellFunction, - pumps, - bytes32(abi.encode("2")) - ); + WellUpgradeable well2 = + encodeAndBoreWellUpgradeable(aquifer, wellImpl, tokens, wellFunction, pumps, bytes32(abi.encode("2"))); vm.label(address(well2), "upgradeableWell2"); vm.startPrank(initialOwner); WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress)); proxy.upgradeTo(address(well2)); assertEq(initialOwner, MockWellUpgradeable(proxyAddress).owner()); // verify proxy was upgraded. - assertEq( - address(well2), - MockWellUpgradeable(proxyAddress).getImplementation() - ); + assertEq(address(well2), MockWellUpgradeable(proxyAddress).getImplementation()); assertEq(1, MockWellUpgradeable(proxyAddress).getVersion()); assertEq(100, MockWellUpgradeable(proxyAddress).getVersion(100)); vm.stopPrank(); @@ -248,14 +210,8 @@ contract WellUpgradeTest is Test, WellDeployer { pumps[0] = Call(mockPumpAddress, abi.encode("2")); // create new mock Well Implementation: address wellImpl = address(new MockWellUpgradeable()); - WellUpgradeable well2 = encodeAndBoreWellUpgradeable( - aquifer, - wellImpl, - tokens, - wellFunction, - pumps, - bytes32(abi.encode("2")) - ); + WellUpgradeable well2 = + encodeAndBoreWellUpgradeable(aquifer, wellImpl, tokens, wellFunction, pumps, bytes32(abi.encode("2"))); vm.label(address(well2), "upgradeableWell2"); // set caller to not be the owner address notOwner = makeAddr("notOwner"); @@ -280,14 +236,8 @@ contract WellUpgradeTest is Test, WellDeployer { // create new mock Well Implementation: address wellImpl = address(new MockWellUpgradeable()); // bore new well with the different tokens - WellUpgradeable well2 = encodeAndBoreWellUpgradeable( - aquifer, - wellImpl, - newTokens, - wellFunction, - pumps, - bytes32(abi.encode("2")) - ); + WellUpgradeable well2 = + encodeAndBoreWellUpgradeable(aquifer, wellImpl, newTokens, wellFunction, pumps, bytes32(abi.encode("2"))); vm.label(address(well2), "upgradeableWell2"); vm.startPrank(initialOwner); WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress)); @@ -308,14 +258,8 @@ contract WellUpgradeTest is Test, WellDeployer { // create new mock Well Implementation: address wellImpl = address(new MockWellUpgradeable()); // bore new well with the different tokens - WellUpgradeable well2 = encodeAndBoreWellUpgradeable( - aquifer, - wellImpl, - newTokens, - wellFunction, - pumps, - bytes32(abi.encode("2")) - ); + WellUpgradeable well2 = + encodeAndBoreWellUpgradeable(aquifer, wellImpl, newTokens, wellFunction, pumps, bytes32(abi.encode("2"))); vm.label(address(well2), "upgradeableWell2"); vm.startPrank(initialOwner); WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress)); From 7ee69fdf5a6ea59d2fb4a57a6669949450263cc4 Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 21 Aug 2024 17:25:35 +0300 Subject: [PATCH 14/22] fix typo in _authorizeUpgrade --- src/WellUpgradeable.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/WellUpgradeable.sol b/src/WellUpgradeable.sol index fe763814..a1c5da68 100644 --- a/src/WellUpgradeable.sol +++ b/src/WellUpgradeable.sol @@ -60,7 +60,7 @@ contract WellUpgradeable is Well, UUPSUpgradeable, OwnableUpgradeable { * @notice Check that the execution is being performed through a delegatecall call and that the execution context is * a proxy contract with an ERC1167 minimal proxy from an aquifier, pointing to a well implmentation. */ - function _authorizeUpgrade(address newImplmentation) internal view override onlyOwner { + function _authorizeUpgrade(address newImplementation) internal view override onlyOwner { // verify the function is called through a delegatecall. require(address(this) != ___self, "Function must be called through delegatecall"); @@ -71,13 +71,13 @@ contract WellUpgradeable is Well, UUPSUpgradeable, OwnableUpgradeable { // verify the new implmentation is a well bored by an aquifier. require( - IAquifer(aquifer).wellImplementation(newImplmentation) != address(0), + IAquifer(aquifer).wellImplementation(newImplementation) != address(0), "New implementation must be a well implmentation" ); // verify the new well uses the same tokens in the same order. IERC20[] memory _tokens = tokens(); - IERC20[] memory newTokens = WellUpgradeable(newImplmentation).tokens(); + IERC20[] memory newTokens = WellUpgradeable(newImplementation).tokens(); require(_tokens.length == newTokens.length, "New well must use the same number of tokens"); for (uint256 i; i < _tokens.length; ++i) { require(_tokens[i] == newTokens[i], "New well must use the same tokens in the same order"); @@ -85,7 +85,7 @@ contract WellUpgradeable is Well, UUPSUpgradeable, OwnableUpgradeable { // verify the new implmentation is a valid ERC-1967 implmentation. require( - UUPSUpgradeable(newImplmentation).proxiableUUID() == _IMPLEMENTATION_SLOT, + UUPSUpgradeable(newImplementation).proxiableUUID() == _IMPLEMENTATION_SLOT, "New implementation must be a valid ERC-1967 implmentation" ); } From 42a4dbc84fc0040f67f9184f5e363319e78845ae Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 21 Aug 2024 17:25:58 +0300 Subject: [PATCH 15/22] clarify comment in calcReserveAtRatioSwap --- src/functions/Stable2.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/Stable2.sol b/src/functions/Stable2.sol index 884f3249..f7ebb332 100644 --- a/src/functions/Stable2.sol +++ b/src/functions/Stable2.sol @@ -245,7 +245,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { pd.currentPrice = pd.newPrice; - // check if new price is within 1 of target price: + // check if new price is within PRICE_THRESHOLD: if (pd.currentPrice > pd.targetPrice) { if (pd.currentPrice - pd.targetPrice <= PRICE_THRESHOLD) { return scaledReserves[j] / (10 ** (18 - decimals[j])); From 6b510150197520e23d4d62d8526f54afcf303a17 Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 21 Aug 2024 17:36:58 +0300 Subject: [PATCH 16/22] change proxiableUUID visibility to public --- src/WellUpgradeable.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WellUpgradeable.sol b/src/WellUpgradeable.sol index a1c5da68..171877e1 100644 --- a/src/WellUpgradeable.sol +++ b/src/WellUpgradeable.sol @@ -121,7 +121,7 @@ contract WellUpgradeable is Well, UUPSUpgradeable, OwnableUpgradeable { * are ERC-1167 minimal immutable clones and cannot delgate to another proxy. Thus, `proxiableUUID` was updated to support * this specific usecase. */ - function proxiableUUID() external view override notDelegatedOrIsMinimalProxy returns (bytes32) { + function proxiableUUID() public view override notDelegatedOrIsMinimalProxy returns (bytes32) { return _IMPLEMENTATION_SLOT; } From ec8e7e3aaa8f598b1f0ebad1ef072e794a6d3e06 Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 21 Aug 2024 17:41:49 +0300 Subject: [PATCH 17/22] remove unreachable code blocks in stable2 --- src/functions/Stable2.sol | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/functions/Stable2.sol b/src/functions/Stable2.sol index f7ebb332..07b3b121 100644 --- a/src/functions/Stable2.sol +++ b/src/functions/Stable2.sol @@ -213,11 +213,8 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { } // calculate max step size: - if (pd.lutData.lowPriceJ > pd.lutData.highPriceJ) { - pd.maxStepSize = scaledReserves[j] * (pd.lutData.lowPriceJ - pd.lutData.highPriceJ) / pd.lutData.lowPriceJ; - } else { - pd.maxStepSize = scaledReserves[j] * (pd.lutData.highPriceJ - pd.lutData.lowPriceJ) / pd.lutData.highPriceJ; - } + // lowPriceJ will always be larger than highPriceJ so a check here is unnecessary. + pd.maxStepSize = scaledReserves[j] * (pd.lutData.lowPriceJ - pd.lutData.highPriceJ) / pd.lutData.lowPriceJ; for (uint256 k; k < 255; k++) { scaledReserves[j] = updateReserve(pd, scaledReserves[j]); @@ -300,11 +297,8 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { } // calculate max step size: - if (pd.lutData.lowPriceJ > pd.lutData.highPriceJ) { - pd.maxStepSize = scaledReserves[j] * (pd.lutData.lowPriceJ - pd.lutData.highPriceJ) / pd.lutData.lowPriceJ; - } else { - pd.maxStepSize = scaledReserves[j] * (pd.lutData.highPriceJ - pd.lutData.lowPriceJ) / pd.lutData.highPriceJ; - } + // lowPriceJ will always be larger than highPriceJ so a check here is unnecessary. + pd.maxStepSize = scaledReserves[j] * (pd.lutData.lowPriceJ - pd.lutData.highPriceJ) / pd.lutData.lowPriceJ; for (uint256 k; k < 255; k++) { scaledReserves[j] = updateReserve(pd, scaledReserves[j]); From ba1ce3f308177c2bafc2224948f8e1cbcdfbe6f2 Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 21 Aug 2024 17:45:34 +0300 Subject: [PATCH 18/22] remove redundant sumReserves check in calcLpTokenSupply --- src/functions/Stable2.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/functions/Stable2.sol b/src/functions/Stable2.sol index 07b3b121..9d9fe4a7 100644 --- a/src/functions/Stable2.sol +++ b/src/functions/Stable2.sol @@ -82,9 +82,8 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { uint256[] memory scaledReserves = getScaledReserves(reserves, decimals); uint256 Ann = a * N * N; - + uint256 sumReserves = scaledReserves[0] + scaledReserves[1]; - if (sumReserves == 0) return 0; lpTokenSupply = sumReserves; for (uint256 i = 0; i < 255; i++) { uint256 dP = lpTokenSupply; From fa50f70d38f7ee1d9601f3fd215254401ea4d60f Mon Sep 17 00:00:00 2001 From: nickkatsios Date: Wed, 21 Aug 2024 17:45:51 +0300 Subject: [PATCH 19/22] add venv files to gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index ff8f73d9..36206d14 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ cache/ out/ +# Python virtual environments +env/ +venv/ + .vscode # Ignores development broadcast logs From c6ce2023954ebffd8bfcfaf13df4643ecd0762f6 Mon Sep 17 00:00:00 2001 From: Brean0 Date: Thu, 22 Aug 2024 09:34:49 +0200 Subject: [PATCH 20/22] update package version. --- package.json | 2 +- src/functions/Stable2.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cc05a548..2c73df3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@beanstalk/wells", - "version": "1.2.0-prerelease2", + "version": "1.2.0-prerelease3", "description": "A [{Well}](/src/Well.sol) is a constant function AMM that allows the provisioning of liquidity into a single pooled on-chain liquidity position.", "main": "index.js", "directories": { diff --git a/src/functions/Stable2.sol b/src/functions/Stable2.sol index 9d9fe4a7..05d2fa55 100644 --- a/src/functions/Stable2.sol +++ b/src/functions/Stable2.sol @@ -82,7 +82,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { uint256[] memory scaledReserves = getScaledReserves(reserves, decimals); uint256 Ann = a * N * N; - + uint256 sumReserves = scaledReserves[0] + scaledReserves[1]; lpTokenSupply = sumReserves; for (uint256 i = 0; i < 255; i++) { From 20bb66aed5dc58c612ee6478b6698951f0a5d4e8 Mon Sep 17 00:00:00 2001 From: Brean0 Date: Thu, 29 Aug 2024 11:54:54 +0200 Subject: [PATCH 21/22] Add check for oscillation in calcLpTokenSupply. --- src/functions/Stable2.sol | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/functions/Stable2.sol b/src/functions/Stable2.sol index 05d2fa55..4f7ac989 100644 --- a/src/functions/Stable2.sol +++ b/src/functions/Stable2.sol @@ -86,6 +86,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { uint256 sumReserves = scaledReserves[0] + scaledReserves[1]; lpTokenSupply = sumReserves; for (uint256 i = 0; i < 255; i++) { + bool stableOscillation; uint256 dP = lpTokenSupply; // If division by 0, this will be borked: only withdrawal will work. And that is good dP = dP * lpTokenSupply / (scaledReserves[0] * N); @@ -93,10 +94,25 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { uint256 prevReserves = lpTokenSupply; lpTokenSupply = (Ann * sumReserves / A_PRECISION + (dP * N)) * lpTokenSupply / (((Ann - A_PRECISION) * lpTokenSupply / A_PRECISION) + ((N + 1) * dP)); + // Equality with the precision of 1 + // If the difference between the current lpTokenSupply and the previous lpTokenSupply is 2, + // Check that the oscillation is stable, and if so, return the average between the two. if (lpTokenSupply > prevReserves) { + if (lpTokenSupply - prevReserves == 2) { + if (stableOscillation) { + return lpTokenSupply - 1; + } + stableOscillation = true; + } if (lpTokenSupply - prevReserves <= 1) return lpTokenSupply; } else { + if (prevReserves - lpTokenSupply == 2) { + if (stableOscillation) { + return lpTokenSupply + 1; + } + stableOscillation = true; + } if (prevReserves - lpTokenSupply <= 1) return lpTokenSupply; } } From ff021fed640021ef10972035b7991b2328ce132e Mon Sep 17 00:00:00 2001 From: Brean0 Date: Thu, 29 Aug 2024 12:57:50 +0200 Subject: [PATCH 22/22] change absolute diff to relative diff, increase price precision. --- src/functions/Stable2.sol | 21 ++++++++++++++++--- ...kStable2.calcReserveAtRatioLiquidity.t.sol | 2 +- ...nstalkStable2.calcReserveAtRatioSwap.t.sol | 10 ++++----- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/functions/Stable2.sol b/src/functions/Stable2.sol index 4f7ac989..430683a3 100644 --- a/src/functions/Stable2.sol +++ b/src/functions/Stable2.sol @@ -42,7 +42,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { // price threshold. more accurate pricing requires a lower threshold, // at the cost of higher execution costs. - uint256 constant PRICE_THRESHOLD = 100; // 0.01% + uint256 constant PRICE_THRESHOLD = 10; // 0.001% address immutable lookupTable; uint256 immutable a; @@ -213,7 +213,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { uint256 parityReserve = lpTokenSupply / 2; // update `scaledReserves` based on whether targetPrice is closer to low or high price: - if (pd.lutData.highPrice - pd.targetPrice > pd.targetPrice - pd.lutData.lowPrice) { + if (percentDiff(pd.lutData.highPrice, pd.targetPrice) > percentDiff(pd.lutData.lowPrice, pd.targetPrice)) { // targetPrice is closer to lowPrice. scaledReserves[i] = parityReserve * pd.lutData.lowPriceI / pd.lutData.precision; scaledReserves[j] = parityReserve * pd.lutData.lowPriceJ / pd.lutData.precision; @@ -297,7 +297,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { // update scaledReserve[j] such that calcRate(scaledReserves, i, j) = low/high Price, // depending on which is closer to targetPrice. - if (pd.lutData.highPrice - pd.targetPrice > pd.targetPrice - pd.lutData.lowPrice) { + if (percentDiff(pd.lutData.highPrice, pd.targetPrice) > percentDiff(pd.lutData.lowPrice, pd.targetPrice)) { // targetPrice is closer to lowPrice. scaledReserves[j] = scaledReserves[i] * pd.lutData.lowPriceJ / pd.lutData.precision; @@ -444,4 +444,19 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { + pd.maxStepSize * (pd.currentPrice - pd.targetPrice) / (pd.lutData.highPrice - pd.lutData.lowPrice); } } + + /** + * @notice Calculate the percentage difference between two numbers. + * @return The percentage difference as a fixed-point number with 18 decimals. + * @dev This function calculates the absolute percentage difference: + * |(a - b)| / ((a + b) / 2) * 100 + * The result is scaled by 1e18 for precision. + */ + function percentDiff(uint256 _a, uint256 _b) internal pure returns (uint256) { + if (_a == _b) return 0; + uint256 difference = _a > _b ? _a - _b : _b - _a; + uint256 average = (_a + _b) / 2; + // Multiply by 100 * 1e18 to get percentage with 18 decimal places + return (difference * 100 * 1e18) / average; + } } diff --git a/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol index 83241aea..9d0395e4 100644 --- a/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol +++ b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol @@ -61,7 +61,7 @@ contract BeanstalkStable2LiquidityTest is TestHelper { uint256 reserve0 = _f.calcReserveAtRatioLiquidity(reserves, 0, ratios, data); uint256 reserve1 = _f.calcReserveAtRatioLiquidity(reserves, 1, ratios, data); - assertApproxEqRel(reserve0, 4.575771214546676444e18, 0.0001e18); + assertApproxEqRel(reserve0, 4.576236561359714812e18, 0.0001e18); assertApproxEqRel(reserve1, 0.21852354514449462e18, 0.0001e18); } diff --git a/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol index 3a491d8c..a21d80fc 100644 --- a/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol +++ b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol @@ -30,8 +30,8 @@ contract BeanstalkStable2SwapTest is TestHelper { uint256 reserve0 = _f.calcReserveAtRatioSwap(reserves, 0, ratios, data); uint256 reserve1 = _f.calcReserveAtRatioSwap(reserves, 1, ratios, data); - assertEq(reserve0, 100.005058322101089709e18); - assertEq(reserve1, 100.005058322101089709e18); + assertEq(reserve0, 99.999921040536083478e18); + assertEq(reserve1, 99.999921040536083478e18); } function test_calcReserveAtRatioSwap_equal_diff() public view { @@ -45,8 +45,8 @@ contract BeanstalkStable2SwapTest is TestHelper { uint256 reserve0 = _f.calcReserveAtRatioSwap(reserves, 0, ratios, data); uint256 reserve1 = _f.calcReserveAtRatioSwap(reserves, 1, ratios, data); - assertEq(reserve0, 73.517644476151580971e18); - assertEq(reserve1, 73.517644476151580971e18); + assertEq(reserve0, 73.513867858788351572e18); + assertEq(reserve1, 73.513867858788351572e18); } function test_calcReserveAtRatioSwap_diff_equal() public view { @@ -61,7 +61,7 @@ contract BeanstalkStable2SwapTest is TestHelper { uint256 reserve1 = _f.calcReserveAtRatioSwap(reserves, 1, ratios, data); assertEq(reserve0, 180.644064978044534737e18); // 180.644064978044534737e18, 100e18 - assertEq(reserve1, 39.475055811844664131e18); // 100e18, 39.475055811844664131e18 + assertEq(reserve1, 39.474244037189430513e18); // 100e18, 39.475055811844664131e18 } function test_calcReserveAtRatioSwap_diff_diff() public view {