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 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 diff --git a/package.json b/package.json index 27dc8b79..2c73df3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@beanstalk/wells", - "version": "1.2.0-prerelease1", + "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/WellUpgradeable.sol b/src/WellUpgradeable.sol index ea60c50d..171877e1 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"); } _; } @@ -62,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 { + 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"); @@ -73,13 +71,21 @@ 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(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"); + } + // 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" ); } @@ -115,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; } diff --git a/src/functions/Stable2.sol b/src/functions/Stable2.sol index ca25edc0..430683a3 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; } @@ -41,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; @@ -83,9 +84,9 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { 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++) { + 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,13 +94,29 @@ 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; } } + revert("Non convergence: calcLpTokenSupply"); } /** @@ -140,7 +157,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { } } } - revert("did not find convergence"); + revert("Non convergence: calcReserve"); } /** @@ -196,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; @@ -211,21 +228,36 @@ 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]); // 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: + // 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])); @@ -236,6 +268,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { } } } + revert("Non convergence: calcReserveAtRatioSwap"); } /** @@ -264,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; @@ -279,16 +312,29 @@ 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]); // 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) { @@ -301,6 +347,7 @@ contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { } } } + revert("Non convergence: calcReserveAtRatioLiquidity"); } /** @@ -314,7 +361,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(); @@ -397,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/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/WellUpgradeable.t.sol b/test/WellUpgradeable.t.sol index a14b291d..e94ee079 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"); @@ -74,14 +76,13 @@ 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. - // 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. @@ -173,18 +174,15 @@ contract WellUpgradeTest is Test, WellDeployer { } function testRevertTransferOwnershipFromNotOnwer() public { - vm.expectRevert(); address notOwner = makeAddr("notOwner"); vm.prank(notOwner); + vm.expectRevert(); WellUpgradeable(proxyAddress).transferOwnership(notOwner); } ////////////////////// 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")); @@ -193,7 +191,6 @@ contract WellUpgradeTest is Test, WellDeployer { 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 +201,71 @@ contract WellUpgradeTest is Test, WellDeployer { assertEq(100, MockWellUpgradeable(proxyAddress).getVersion(100)); vm.stopPrank(); } + + ///////////////// Access Control //////////////////// + + function testUpgradeToNewImplementationAccessControl() public { + 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("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"))); + 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(); + } } diff --git a/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol index a229a992..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); } @@ -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..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 { @@ -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.474244037189430513e18); // 100e18, 39.475055811844664131e18 } function test_calcReserveAtRatioSwap_diff_diff() public view { @@ -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); } @@ -101,4 +107,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); + } } diff --git a/test/functions/Stable2.t.sol b/test/functions/Stable2.t.sol index 3cff992b..eeb9c731 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/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); 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/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); 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);