diff --git a/.gitignore b/.gitignore index e3a12683..36206d14 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,12 @@ cache/ out/ +# Python virtual environments +env/ +venv/ + +.vscode + # Ignores development broadcast logs /broadcast diff --git a/foundry.toml b/foundry.toml index f4080ba9..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 = 800 +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/lib/forge-std b/lib/forge-std index 5125ce50..07263d19 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 5125ce505fa60ca747df08ab2cc77db5445bd716 +Subproject commit 07263d193d621c4b2b0ce8b4d54af58f6957d97d diff --git a/mocks/wells/MockWellUpgradeable.sol b/mocks/wells/MockWellUpgradeable.sol new file mode 100644 index 00000000..6e6513e0 --- /dev/null +++ b/mocks/wells/MockWellUpgradeable.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {WellUpgradeable} from "src/WellUpgradeable.sol"; + +// this needs to be here for upgrade checks +/// @custom:oz-upgrades-from WellUpgradeable +contract MockWellUpgradeable is WellUpgradeable { + + function getVersion(uint256 i) external pure returns (uint256) { + return i; + } +} \ No newline at end of file diff --git a/package.json b/package.json index f0699859..82da82b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@beanstalk/wells", - "version": "1.1.0", + "version": "1.2.0", "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/requirements.txt b/requirements.txt index 44563cd0..65541fce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,16 @@ -eth-abi==3.0.1 -pandas -numpy \ No newline at end of file +cytoolz==0.12.3 +eth-hash==0.7.0 +eth-typing==3.5.2 +eth-utils==2.3.1 +eth_abi==5.1.0 +numpy==2.0.1 +pandas==2.2.2 +parsimonious==0.10.0 +python-dateutil==2.9.0.post0 +pytz==2024.1 +regex==2024.5.15 +setuptools==71.1.0 +six==1.16.0 +toolz==0.12.1 +typing_extensions==4.12.2 +tzdata==2024.1 diff --git a/script/helpers/WellDeployer.sol b/script/helpers/WellDeployer.sol index 8a42e3d2..028a437e 100644 --- a/script/helpers/WellDeployer.sol +++ b/script/helpers/WellDeployer.sol @@ -5,6 +5,8 @@ import {LibContractInfo} from "src/libraries/LibContractInfo.sol"; import {LibWellConstructor} from "src/libraries/LibWellConstructor.sol"; import {Well, Call, IERC20} from "src/Well.sol"; import {Aquifer} from "src/Aquifer.sol"; +import {WellUpgradeable} from "src/WellUpgradeable.sol"; +import {LibWellUpgradeableConstructor} from "src/libraries/LibWellUpgradeableConstructor.sol"; abstract contract WellDeployer { /** @@ -28,4 +30,26 @@ abstract contract WellDeployer { LibWellConstructor.encodeWellDeploymentData(_aquifer, _tokens, _wellFunction, _pumps); _well = Well(Aquifer(_aquifer).boreWell(_wellImplementation, immutableData, initData, _salt)); } + + /** + * @notice Encode the Well's immutable data, and deploys the well. Modified for upgradeable wells. + * @param _aquifer The address of the Aquifer which will deploy this Well. + * @param _wellImplementation The address of the Well implementation. + * @param _tokens A list of ERC20 tokens supported by the Well. + * @param _wellFunction A single Call struct representing a call to the Well Function. + * @param _pumps An array of Call structs representings calls to Pumps. + * @param _salt The salt to deploy the Well with (`bytes32(0)` for none). See {LibClone}. + */ + function encodeAndBoreWellUpgradeable( + address _aquifer, + address _wellImplementation, + IERC20[] memory _tokens, + Call memory _wellFunction, + Call[] memory _pumps, + bytes32 _salt + ) internal returns (WellUpgradeable _well) { + (bytes memory immutableData, bytes memory initData) = + LibWellUpgradeableConstructor.encodeWellDeploymentData(_aquifer, _tokens, _wellFunction, _pumps); + _well = WellUpgradeable(Aquifer(_aquifer).boreWell(_wellImplementation, immutableData, initData, _salt)); + } } diff --git a/script/simulations/stableswap/StableswapCalcRatiosLiqSim.s.sol b/script/simulations/stableswap/StableswapCalcRatiosLiqSim.s.sol new file mode 100644 index 00000000..16912c41 --- /dev/null +++ b/script/simulations/stableswap/StableswapCalcRatiosLiqSim.s.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {Stable2} from "src/functions/Stable2.sol"; +import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; + +/** + * Stable2 well function simulation and precalculations used + * to produce the token ratios for the lookup table needed for the initial + * `calcReserveAtRatioLiquidity` estimates. + */ +contract StableswapCalcRatiosLiqSim is Script { + function run() external { + Stable2LUT1 stable2LUT1 = new Stable2LUT1(); + Stable2 stable2 = new Stable2(address(stable2LUT1)); + console.log("stable2.getAParameter(): %d", stable2LUT1.getAParameter()); + // initial reserves + uint256 init_reserve_x = 1_000_000e18; + uint256 init_reserve_y = 1_000_000e18; + uint256[] memory reserves = new uint256[](2); + reserves[0] = init_reserve_x; + reserves[1] = init_reserve_y; + uint256 reserve_y = init_reserve_y; + bytes memory data = abi.encode(18, 18); + uint256 price; + + // for n times (1...n) : + // 1) modify reserve x_n-1 by some percentage (this changes the pool liquidity) + // 3) calc price_n using calcRate(...) + + // csv header + console.log("Price (P),Reserve (x),Reserve (y)"); + + // calcReserveAtRatioLiquidity + for (uint256 i; i < 20; i++) { + // update reserves + reserve_y = reserve_y * 88 / 100; + reserves[1] = reserve_y; + // mark price + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, init_reserve_x, reserve_y); + } + + // reset reserves + reserve_y = init_reserve_y; + + // calcReserveAtRatioLiquidity + for (uint256 i; i < 20; i++) { + // update reserves + reserve_y = reserve_y * 98 / 100; + reserves[1] = reserve_y; + // mark price + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, init_reserve_x, reserve_y); + } + + // reset reserves + reserve_y = init_reserve_y; + + // calcReserveAtRatioLiquidity + for (uint256 i; i < 20; i++) { + // update reserves + reserve_y = reserve_y * 102 / 100; + reserves[1] = reserve_y; + // mark price + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, init_reserve_x, reserve_y); + } + + // reset reserves + reserve_y = init_reserve_y; + + // calcReserveAtRatioLiquidity + for (uint256 i; i < 20; i++) { + // update reserves + reserve_y = reserve_y * 112 / 100; + reserves[1] = reserve_y; + // mark price + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, init_reserve_x, reserve_y); + } + + // Extreme prices + + // extreme low + reserve_y = init_reserve_y * 1 / 28; + reserves[1] = reserve_y; + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, init_reserve_x, reserve_y); + + // extreme high + reserve_y = init_reserve_y * 2000; + reserves[1] = reserve_y; + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, init_reserve_x, reserve_y); + } +} diff --git a/script/simulations/stableswap/StableswapCalcRatiosSwapSim.s.sol b/script/simulations/stableswap/StableswapCalcRatiosSwapSim.s.sol new file mode 100644 index 00000000..1337f6a9 --- /dev/null +++ b/script/simulations/stableswap/StableswapCalcRatiosSwapSim.s.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {Stable2} from "src/functions/Stable2.sol"; +import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; + +/** + * Stable2 well function simulation and precalculations used + * to produce the token ratios for the lookup table needed for the initial + * `calcReserveAtRatioSwap` estimates. + */ +contract StableswapCalcRatiosSwapSim is Script { + function run() external { + Stable2LUT1 stable2LUT1 = new Stable2LUT1(); + Stable2 stable2 = new Stable2(address(stable2LUT1)); + console.log("stable2.getAParameter(): %d", stable2LUT1.getAParameter()); + // initial reserves + uint256 init_reserve_x = 1_000_000e18; + uint256 init_reserve_y = 1_000_000e18; + uint256[] memory reserves = new uint256[](2); + reserves[0] = init_reserve_x; + reserves[1] = init_reserve_y; + bytes memory data = abi.encode(18, 18); + // calculateLP token supply (this remains unchanged) + uint256 lpTokenSupply = stable2.calcLpTokenSupply(reserves, data); + console.log("lp_token_supply: %d", lpTokenSupply); + uint256 reserve_x = init_reserve_x; + uint256 price; + + // for n times (1...n) : + // 1) increment x_n-1 by some amount to get x_n + // 2) calc y_n using calcReserves(...) + // 3) calc price_n using calcRate(...) + + // csv header + console.log("Price (P),Reserve (x),Reserve (y)"); + + for (uint256 i; i < 20; i++) { + // update reserve x + reserve_x = reserve_x * 92 / 100; + reserves[0] = reserve_x; + // get y_n --> corresponding reserve y for a given liquidity level + uint256 reserve_y = stable2.calcReserve(reserves, 1, lpTokenSupply, data); + // update reserve y + reserves[1] = reserve_y; + // mark price + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, reserve_x, reserve_y); + } + + // reset reserves + reserve_x = init_reserve_x; + + for (uint256 i; i < 40; i++) { + // update reserve x + reserve_x = reserve_x * 99 / 100; + reserves[0] = reserve_x; + // get y_n --> corresponding reserve y for a given liquidity level + uint256 reserve_y = stable2.calcReserve(reserves, 1, lpTokenSupply, data); + // update reserve y + reserves[1] = reserve_y; + // mark price + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, reserve_x, reserve_y); + } + + // reset reserves + reserve_x = init_reserve_x; + + for (uint256 i; i < 40; i++) { + // update reserve x + reserve_x = reserve_x * 101 / 100; + reserves[0] = reserve_x; + // get y_n --> corresponding reserve y for a given liquidity level + uint256 reserve_y = stable2.calcReserve(reserves, 1, lpTokenSupply, data); + // update reserve y + reserves[1] = reserve_y; + // mark price + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, reserve_x, reserve_y); + } + + // reset reserves + reserve_x = init_reserve_x; + + for (uint256 i; i < 18; i++) { + // update reserve x + reserve_x = reserve_x * 105 / 100; + reserves[0] = reserve_x; + // get y_n --> corresponding reserve y for a given liquidity level + uint256 reserve_y = stable2.calcReserve(reserves, 1, lpTokenSupply, data); + // update reserve y + reserves[1] = reserve_y; + // mark price + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, reserve_x, reserve_y); + } + + // Extreme prices + + // extreme low + reserve_x = init_reserve_x * 3; + reserves[0] = reserve_x; + uint256 reserve_y = stable2.calcReserve(reserves, 1, lpTokenSupply, data); + reserves[1] = reserve_y; + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, reserve_x, reserve_y); + + // extreme high + reserve_x = init_reserve_x * 1 / 190; + reserves[0] = reserve_x; + reserve_y = stable2.calcReserve(reserves, 1, lpTokenSupply, data); + reserves[1] = reserve_y; + price = stable2.calcRate(reserves, 0, 1, data); + console.log("%d,%d,%d", price, reserve_x, reserve_y); + } +} diff --git a/src/Well.sol b/src/Well.sol index d5b32853..f14793f4 100644 --- a/src/Well.sol +++ b/src/Well.sol @@ -44,7 +44,7 @@ contract Well is ERC20PermitUpgradeable, IWell, IWellErrors, ReentrancyGuardUpgr _disableInitializers(); } - function init(string memory _name, string memory _symbol) external initializer { + function init(string memory _name, string memory _symbol) external virtual initializer { __ERC20Permit_init(_name); __ERC20_init(_name, _symbol); __ReentrancyGuard_init(); @@ -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); @@ -713,7 +704,9 @@ contract Well is ERC20PermitUpgradeable, IWell, IWellErrors, ReentrancyGuardUpgr */ function _setReserves(IERC20[] memory _tokens, uint256[] memory reserves) internal { for (uint256 i; i < reserves.length; ++i) { - if (reserves[i] > _tokens[i].balanceOf(address(this))) revert InvalidReserves(); + if (reserves[i] > _tokens[i].balanceOf(address(this))) { + revert InvalidReserves(); + } } LibBytes.storeUint128(RESERVES_STORAGE_SLOT, reserves); } diff --git a/src/WellUpgradeable.sol b/src/WellUpgradeable.sol new file mode 100644 index 00000000..171877e1 --- /dev/null +++ b/src/WellUpgradeable.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Well} from "src/Well.sol"; +import {UUPSUpgradeable} from "ozu/proxy/utils/UUPSUpgradeable.sol"; +import {OwnableUpgradeable} from "ozu/access/OwnableUpgradeable.sol"; +import {IERC20, SafeERC20} from "oz/token/ERC20/utils/SafeERC20.sol"; +import {IAquifer} from "src/interfaces/IAquifer.sol"; + +/** + * @title WellUpgradeable + * @author Deadmanwalking, Brean, Brendan, Silo Chad + * @notice WellUpgradeable is an upgradeable version of the Well contract. + */ +contract WellUpgradeable is Well, UUPSUpgradeable, OwnableUpgradeable { + address private immutable ___self = address(this); + + /** + * @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"); + } + _; + } + + function init(string memory _name, string memory _symbol) external override reinitializer(2) { + __ERC20Permit_init(_name); + __ERC20_init(_name, _symbol); + __ReentrancyGuard_init(); + __UUPSUpgradeable_init(); + __Ownable_init(); + + IERC20[] memory _tokens = tokens(); + uint256 tokensLength = _tokens.length; + for (uint256 i; i < tokensLength - 1; ++i) { + for (uint256 j = i + 1; j < tokensLength; ++j) { + if (_tokens[i] == _tokens[j]) { + revert DuplicateTokens(_tokens[i]); + } + } + } + } + + /** + * @notice `initNoWellToken` allows for the Well to be initialized without deploying a Well token. + */ + function initNoWellToken() external initializer {} + + // Wells deployed by aquifers use the EIP-1167 minimal proxy pattern for gas-efficent deployments. + // This pattern breaks the UUPS upgrade pattern, as the `__self` variable is set to the initial well implmentation. + // `_authorizeUpgrade` and `upgradeTo` are modified to allow for upgrades to the Well implementation. + // verification is done by verifying the ERC1967 implmentation (the well address) maps to the aquifers well -> implmentation mapping. + + /** + * @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 newImplementation) internal view override onlyOwner { + // verify the function is called through a delegatecall. + require(address(this) != ___self, "Function must be called through delegatecall"); + + // verify the function is called through an active proxy bored by an aquifer. + address aquifer = aquifer(); + address activeProxy = IAquifer(aquifer).wellImplementation(_getImplementation()); + require(activeProxy == ___self, "Function must be called through active proxy bored by an aquifer"); + + // verify the new implmentation is a well bored by an aquifier. + require( + 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(newImplementation).proxiableUUID() == _IMPLEMENTATION_SLOT, + "New implementation must be a valid ERC-1967 implmentation" + ); + } + + /** + * @notice Upgrades the implementation of the proxy to `newImplementation`. + * Calls {_authorizeUpgrade}. + * @dev `upgradeTo` was modified to support ERC-1167 minimal proxies + * cloned (Bored) by an Aquifer. + */ + function upgradeTo(address newImplementation) public override { + _authorizeUpgrade(newImplementation); + _upgradeToAndCallUUPS(newImplementation, new bytes(0), false); + } + + /** + * @notice Upgrades the implementation of the proxy to `newImplementation`. + * Calls {_authorizeUpgrade}. + * @dev `upgradeTo` was modified to support ERC-1167 minimal proxies + * cloned (Bored) by an Aquifer. + */ + function upgradeToAndCall(address newImplementation, bytes memory data) public payable override { + _authorizeUpgrade(newImplementation); + _upgradeToAndCallUUPS(newImplementation, data, true); + } + + /** + * @dev Implementation of the ERC1822 {proxiableUUID} function. This returns the storage slot used by the + * implementation. It is used to validate the implementation's compatibility when performing an upgrade. + * + * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks + * bricking a proxy that upgrades to it, by delegating to itself until out of gas. However, Wells bored by Aquifers + * are ERC-1167 minimal immutable clones and cannot delgate to another proxy. Thus, `proxiableUUID` was updated to support + * this specific usecase. + */ + function proxiableUUID() public view override notDelegatedOrIsMinimalProxy returns (bytes32) { + return _IMPLEMENTATION_SLOT; + } + + function getImplementation() external view returns (address) { + return _getImplementation(); + } + + function getVersion() external pure virtual returns (uint256) { + return 1; + } + + function getInitializerVersion() external view returns (uint256) { + return _getInitializedVersion(); + } +} diff --git a/src/functions/Stable2.sol b/src/functions/Stable2.sol new file mode 100644 index 00000000..430683a3 --- /dev/null +++ b/src/functions/Stable2.sol @@ -0,0 +1,462 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IBeanstalkWellFunction, IMultiFlowPumpWellFunction} from "src/interfaces/IBeanstalkWellFunction.sol"; +import {ILookupTable} from "src/interfaces/ILookupTable.sol"; +import {ProportionalLPToken2} from "src/functions/ProportionalLPToken2.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +/** + * @author brean, deadmanwalking + * @title Gas efficient StableSwap pricing function for Wells with 2 tokens. + * developed by curve. + * + * Stableswap Wells with 2 tokens use the formula: + * `4 * A * (b_0+b_1) + D = 4 * A * D + D^3/(4 * b_0 * b_1)` + * + * Where: + * `A` is the Amplication parameter. + * `D` is the supply of LP tokens + * `b_i` is the reserve at index `i` + * + * @dev Limited to tokens with a maximum of 18 decimals. + */ +contract Stable2 is ProportionalLPToken2, IBeanstalkWellFunction { + struct PriceData { + uint256 targetPrice; + uint256 currentPrice; + uint256 newPrice; + uint256 maxStepSize; + ILookupTable.PriceData lutData; + } + + // 2 token Pool. + uint256 constant N = 2; + + // A precision + uint256 constant A_PRECISION = 100; + + // price precision. + uint256 constant PRICE_PRECISION = 1e6; + + // price threshold. more accurate pricing requires a lower threshold, + // at the cost of higher execution costs. + uint256 constant PRICE_THRESHOLD = 10; // 0.001% + + address immutable lookupTable; + uint256 immutable a; + + // Errors + error InvalidTokenDecimals(); + error InvalidLUT(); + + // Due to the complexity of `calcReserveAtRatioLiquidity` and `calcReserveAtRatioSwap`, + // a LUT is used to reduce the complexity of the calculations on chain. + // the lookup table contract implements 3 functions: + // 1. getRatiosFromPriceLiquidity(uint256) -> PriceData memory + // 2. getRatiosFromPriceSwap(uint256) -> PriceData memory + // 3. getAParameter() -> uint256 + // Lookup tables are a function of the A parameter. + constructor(address lut) { + if (lut == address(0)) revert InvalidLUT(); + lookupTable = lut; + a = ILookupTable(lut).getAParameter(); + } + + /** + * @notice Calculate the amount of lp tokens given reserves. + * D invariant calculation in non-overflowing integer operations iteratively + * A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) + * + * Converging solution: + * D[j+1] = (4 * A * sum(b_i) - (D[j] ** 3) / (4 * prod(b_i))) / (4 * A - 1) + */ + function calcLpTokenSupply( + uint256[] memory reserves, + bytes memory data + ) public view returns (uint256 lpTokenSupply) { + if (reserves[0] == 0 && reserves[1] == 0) return 0; + uint256[] memory decimals = decodeWellData(data); + // scale reserves to 18 decimals. + 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++) { + 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); + dP = dP * lpTokenSupply / (scaledReserves[1] * N); + 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"); + } + + /** + * @notice Calculate x[i] if one reduces D from being calculated for reserves to D + * Done by solving quadratic equation iteratively. + * x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + * x_1**2 + b*x_1 = c + * x_1 = (x_1**2 + c) / (2*x_1 + b) + * @dev This function has a precision of +/- 1, + * which may round in favor of the well or the user. + */ + function calcReserve( + uint256[] memory reserves, + uint256 j, + uint256 lpTokenSupply, + bytes memory data + ) public view returns (uint256 reserve) { + uint256[] memory decimals = decodeWellData(data); + uint256[] memory scaledReserves = getScaledReserves(reserves, decimals); + + // avoid stack too deep errors. + (uint256 c, uint256 b) = getBandC(a * N * N, lpTokenSupply, j == 0 ? scaledReserves[1] : scaledReserves[0]); + reserve = lpTokenSupply; + uint256 prevReserve; + + for (uint256 i; i < 255; ++i) { + prevReserve = reserve; + reserve = _calcReserve(reserve, b, c, lpTokenSupply); + // Equality with the precision of 1 + // scale reserve down to original precision + if (reserve > prevReserve) { + if (reserve - prevReserve <= 1) { + return reserve / (10 ** (18 - decimals[j])); + } + } else { + if (prevReserve - reserve <= 1) { + return reserve / (10 ** (18 - decimals[j])); + } + } + } + revert("Non convergence: calcReserve"); + } + + /** + * @inheritdoc IMultiFlowPumpWellFunction + * @dev Returns a rate with decimal precision. + * Requires a minimum scaled reserves of 1e12. + * 6 decimals was chosen as higher decimals would require a higher minimum scaled reserve, + * which is prohibtive for large value tokens. + */ + function calcRate( + uint256[] memory reserves, + uint256 i, + uint256 j, + bytes memory data + ) public view returns (uint256 rate) { + uint256[] memory decimals = decodeWellData(data); + uint256[] memory scaledReserves = getScaledReserves(reserves, decimals); + + // calc lp token supply (note: `scaledReserves` is scaled up, and does not require bytes). + uint256 lpTokenSupply = calcLpTokenSupply(scaledReserves, abi.encode(18, 18)); + + rate = _calcRate(scaledReserves, i, j, lpTokenSupply); + } + + /** + * @inheritdoc IMultiFlowPumpWellFunction + * @dev `calcReserveAtRatioSwap` fetches the closes approximate ratios from the target price, + * and performs newtons method in order to converge into a reserve. + */ + function calcReserveAtRatioSwap( + uint256[] memory reserves, + uint256 j, + uint256[] memory ratios, + bytes calldata data + ) external view returns (uint256 reserve) { + uint256 i = j == 1 ? 0 : 1; + // scale reserves and ratios: + uint256[] memory decimals = decodeWellData(data); + uint256[] memory scaledReserves = getScaledReserves(reserves, decimals); + + PriceData memory pd; + uint256[] memory scaledRatios = getScaledReserves(ratios, decimals); + // calc target price with 6 decimal precision: + pd.targetPrice = scaledRatios[i] * PRICE_PRECISION / scaledRatios[j]; + + // get ratios and price from the closest highest and lowest price from targetPrice: + pd.lutData = ILookupTable(lookupTable).getRatiosFromPriceSwap(pd.targetPrice); + + // calculate lp token supply: + uint256 lpTokenSupply = calcLpTokenSupply(scaledReserves, abi.encode(18, 18)); + + // lpTokenSupply / 2 gives the reserves at parity: + uint256 parityReserve = lpTokenSupply / 2; + + // update `scaledReserves` based on whether targetPrice is closer to low or high price: + 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; + // initialize currentPrice: + pd.currentPrice = pd.lutData.lowPrice; + } else { + // targetPrice is closer to highPrice. + scaledReserves[i] = parityReserve * pd.lutData.highPriceI / pd.lutData.precision; + scaledReserves[j] = parityReserve * pd.lutData.highPriceJ / pd.lutData.precision; + // initialize currentPrice: + pd.currentPrice = pd.lutData.highPrice; + } + + // calculate max step size: + // 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)); + // 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 PRICE_THRESHOLD: + if (pd.currentPrice > pd.targetPrice) { + if (pd.currentPrice - pd.targetPrice <= PRICE_THRESHOLD) { + return scaledReserves[j] / (10 ** (18 - decimals[j])); + } + } else { + if (pd.targetPrice - pd.currentPrice <= PRICE_THRESHOLD) { + return scaledReserves[j] / (10 ** (18 - decimals[j])); + } + } + } + revert("Non convergence: calcReserveAtRatioSwap"); + } + + /** + * @inheritdoc IBeanstalkWellFunction + * @dev `calcReserveAtRatioLiquidity` fetches the closes approximate ratios from the target price, + * and performs newtons method in order to converge into a reserve. + */ + function calcReserveAtRatioLiquidity( + uint256[] calldata reserves, + uint256 j, + uint256[] calldata ratios, + bytes calldata data + ) external view returns (uint256 reserve) { + uint256 i = j == 1 ? 0 : 1; + // scale reserves and ratios: + uint256[] memory decimals = decodeWellData(data); + uint256[] memory scaledReserves = getScaledReserves(reserves, decimals); + + PriceData memory pd; + uint256[] memory scaledRatios = getScaledReserves(ratios, decimals); + // calc target price with 6 decimal precision: + pd.targetPrice = scaledRatios[i] * PRICE_PRECISION / scaledRatios[j]; + + // get ratios and price from the closest highest and lowest price from targetPrice: + pd.lutData = ILookupTable(lookupTable).getRatiosFromPriceLiquidity(pd.targetPrice); + + // update scaledReserve[j] such that calcRate(scaledReserves, i, j) = low/high Price, + // depending on which is closer to targetPrice. + 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; + + // set current price to lowPrice. + pd.currentPrice = pd.lutData.lowPrice; + } else { + // targetPrice is closer to highPrice. + scaledReserves[j] = scaledReserves[i] * pd.lutData.highPriceJ / pd.lutData.precision; + + // set current price to highPrice. + pd.currentPrice = pd.lutData.highPrice; + } + + // calculate max step size: + // 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.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) { + if (pd.currentPrice - pd.targetPrice <= PRICE_THRESHOLD) { + return scaledReserves[j] / (10 ** (18 - decimals[j])); + } + } else { + if (pd.targetPrice - pd.currentPrice <= PRICE_THRESHOLD) { + return scaledReserves[j] / (10 ** (18 - decimals[j])); + } + } + } + revert("Non convergence: calcReserveAtRatioLiquidity"); + } + + /** + * @notice decodes the data encoded in the well. + * @return decimals an array of the decimals of the tokens in the well. + */ + function decodeWellData(bytes memory data) public view virtual returns (uint256[] memory decimals) { + (uint256 decimal0, uint256 decimal1) = abi.decode(data, (uint256, uint256)); + + // if well data returns 0, assume 18 decimals. + if (decimal0 == 0) { + decimal0 = 18; + } + if (decimal1 == 0) { + decimal1 = 18; + } + if (decimal0 > 18 || decimal1 > 18) revert InvalidTokenDecimals(); + + decimals = new uint256[](2); + decimals[0] = decimal0; + decimals[1] = decimal1; + } + + function name() external pure returns (string memory) { + return "Stable2"; + } + + function symbol() external pure returns (string memory) { + return "S2"; + } + + /** + * @notice internal calcRate function. + */ + function _calcRate( + uint256[] memory reserves, + uint256 i, + uint256 j, + uint256 lpTokenSupply + ) internal view returns (uint256 rate) { + // add 1e6 to reserves: + uint256[] memory _reserves = new uint256[](2); + _reserves[i] = reserves[i]; + _reserves[j] = reserves[j] + PRICE_PRECISION; + + // calculate rate: + rate = _reserves[i] - calcReserve(_reserves, i, lpTokenSupply, abi.encode(18, 18)); + } + + /** + * @notice scale `reserves` by `precision`. + * @dev this sets both reserves to 18 decimals. + */ + function getScaledReserves( + uint256[] memory reserves, + uint256[] memory decimals + ) internal pure returns (uint256[] memory scaledReserves) { + scaledReserves = new uint256[](2); + scaledReserves[0] = reserves[0] * 10 ** (18 - decimals[0]); + scaledReserves[1] = reserves[1] * 10 ** (18 - decimals[1]); + } + + function _calcReserve( + uint256 reserve, + uint256 b, + uint256 c, + uint256 lpTokenSupply + ) private pure returns (uint256) { + return (reserve * reserve + c) / (reserve * 2 + b - lpTokenSupply); + } + + function getBandC( + uint256 Ann, + uint256 lpTokenSupply, + uint256 reserves + ) private pure returns (uint256 c, uint256 b) { + c = lpTokenSupply * lpTokenSupply / (reserves * N) * lpTokenSupply * A_PRECISION / (Ann * N); + b = reserves + (lpTokenSupply * A_PRECISION / Ann); + } + + /** + * @notice calculates the step size, and returns the updated reserve. + */ + function updateReserve(PriceData memory pd, uint256 reserve) internal pure returns (uint256) { + if (pd.targetPrice > pd.currentPrice) { + // if the targetPrice is greater than the currentPrice, + // the reserve needs to be decremented to increase currentPrice. + return reserve + - pd.maxStepSize * (pd.targetPrice - pd.currentPrice) / (pd.lutData.highPrice - pd.lutData.lowPrice); + } else { + // if the targetPrice is less than the currentPrice, + // the reserve needs to be incremented to decrease currentPrice. + return reserve + + 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/src/functions/StableLUT/Stable2LUT1.sol b/src/functions/StableLUT/Stable2LUT1.sol new file mode 100644 index 00000000..9a78d7c3 --- /dev/null +++ b/src/functions/StableLUT/Stable2LUT1.sol @@ -0,0 +1,2175 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ILookupTable} from "src/interfaces/ILookupTable.sol"; + +/** + * @title Stable2LUT1 + * @author Deadmanwalking, brean + * @notice Implements a lookup table of estimations used in the Stableswap Well Function + * to calculate the token ratios in a Stableswap pool for a given price. + */ +contract Stable2LUT1 is ILookupTable { + /** + * @notice Returns the amplification coefficient (A parameter) used to calculate the estimates. + * @return The amplification coefficient. + * @dev 2 decimal precision. + */ + function getAParameter() external pure returns (uint256) { + return 100; + } + + /** + * @notice Returns the estimated range of reserve ratios for a given price, + * assuming one token reserve remains constant. + */ + function getRatiosFromPriceLiquidity(uint256 price) external pure returns (PriceData memory) { + if (price < 1.006758e6) { + if (price < 0.885627e6) { + if (price < 0.59332e6) { + if (price < 0.404944e6) { + if (price < 0.30624e6) { + if (price < 0.27702e6) { + if (price < 0.001083e6) { + revert("LUT: Invalid price"); + } else { + return + PriceData(0.27702e6, 0, 9.646293093274934449e18, 0.001083e6, 0, 2000e18, 1e18); + } + } else { + return PriceData( + 0.30624e6, 0, 8.612761690424049377e18, 0.27702e6, 0, 9.646293093274934449e18, 1e18 + ); + } + } else { + if (price < 0.370355e6) { + if (price < 0.337394e6) { + return PriceData( + 0.337394e6, + 0, + 7.689965795021471706e18, + 0.30624e6, + 0, + 8.612761690424049377e18, + 1e18 + ); + } else { + return PriceData( + 0.370355e6, + 0, + 6.866040888412029197e18, + 0.337394e6, + 0, + 7.689965795021471706e18, + 1e18 + ); + } + } else { + return PriceData( + 0.404944e6, 0, 6.130393650367882863e18, 0.370355e6, 0, 6.866040888412029197e18, 1e18 + ); + } + } + } else { + if (price < 0.516039e6) { + if (price < 0.478063e6) { + if (price < 0.440934e6) { + return PriceData( + 0.440934e6, + 0, + 5.473565759257038366e18, + 0.404944e6, + 0, + 6.130393650367882863e18, + 1e18 + ); + } else { + return PriceData( + 0.478063e6, + 0, + 4.887112285050926097e18, + 0.440934e6, + 0, + 5.473565759257038366e18, + 1e18 + ); + } + } else { + return PriceData( + 0.516039e6, 0, 4.363493111652613443e18, 0.478063e6, 0, 4.887112285050926097e18, 1e18 + ); + } + } else { + if (price < 0.554558e6) { + return PriceData( + 0.554558e6, 0, 3.89597599254697613e18, 0.516039e6, 0, 4.363493111652613443e18, 1e18 + ); + } else { + return PriceData( + 0.59332e6, 0, 3.478549993345514402e18, 0.554558e6, 0, 3.89597599254697613e18, 1e18 + ); + } + } + } + } else { + if (price < 0.782874e6) { + if (price < 0.708539e6) { + if (price < 0.670518e6) { + if (price < 0.632052e6) { + return PriceData( + 0.632052e6, + 0, + 3.105848208344209382e18, + 0.59332e6, + 0, + 3.478549993345514402e18, + 1e18 + ); + } else { + return PriceData( + 0.670518e6, + 0, + 2.773078757450186949e18, + 0.632052e6, + 0, + 3.105848208344209382e18, + 1e18 + ); + } + } else { + return PriceData( + 0.708539e6, 0, 2.475963176294809553e18, 0.670518e6, 0, 2.773078757450186949e18, 1e18 + ); + } + } else { + if (price < 0.746003e6) { + return PriceData( + 0.746003e6, 0, 2.210681407406080101e18, 0.708539e6, 0, 2.475963176294809553e18, 1e18 + ); + } else { + return PriceData( + 0.782874e6, 0, 1.973822685183999948e18, 0.746003e6, 0, 2.210681407406080101e18, 1e18 + ); + } + } + } else { + if (price < 0.873157e6) { + if (price < 0.855108e6) { + if (price < 0.819199e6) { + return PriceData( + 0.819199e6, + 0, + 1.762341683200000064e18, + 0.782874e6, + 0, + 1.973822685183999948e18, + 1e18 + ); + } else { + return PriceData( + 0.855108e6, + 0, + 1.573519359999999923e18, + 0.819199e6, + 0, + 1.762341683200000064e18, + 1e18 + ); + } + } else { + return PriceData( + 0.873157e6, 0, 1.485947395978354457e18, 0.855108e6, 0, 1.573519359999999923e18, 1e18 + ); + } + } else { + if (price < 0.879393e6) { + return PriceData( + 0.879393e6, 0, 1.456811172527798348e18, 0.873157e6, 0, 1.485947395978354457e18, 1e18 + ); + } else { + return PriceData( + 0.885627e6, 0, 1.428246247576273165e18, 0.879393e6, 0, 1.456811172527798348e18, 1e18 + ); + } + } + } + } + } else { + if (price < 0.94201e6) { + if (price < 0.916852e6) { + if (price < 0.898101e6) { + if (price < 0.891863e6) { + if (price < 0.89081e6) { + return PriceData( + 0.89081e6, + 0, + 1.404927999999999955e18, + 0.885627e6, + 0, + 1.428246247576273165e18, + 1e18 + ); + } else { + return PriceData( + 0.891863e6, + 0, + 1.400241419192424397e18, + 0.89081e6, + 0, + 1.404927999999999955e18, + 1e18 + ); + } + } else { + return PriceData( + 0.898101e6, 0, 1.372785705090612263e18, 0.891863e6, 0, 1.400241419192424397e18, 1e18 + ); + } + } else { + if (price < 0.910594e6) { + if (price < 0.904344e6) { + return PriceData( + 0.904344e6, + 0, + 1.345868338324129665e18, + 0.898101e6, + 0, + 1.372785705090612263e18, + 1e18 + ); + } else { + return PriceData( + 0.910594e6, + 0, + 1.319478763062872151e18, + 0.904344e6, + 0, + 1.345868338324129665e18, + 1e18 + ); + } + } else { + return PriceData( + 0.916852e6, 0, 1.293606630453796313e18, 0.910594e6, 0, 1.319478763062872151e18, 1e18 + ); + } + } + } else { + if (price < 0.929402e6) { + if (price < 0.9266e6) { + if (price < 0.92312e6) { + return PriceData( + 0.92312e6, + 0, + 1.268241794562545266e18, + 0.916852e6, + 0, + 1.293606630453796313e18, + 1e18 + ); + } else { + return PriceData( + 0.9266e6, + 0, + 1.254399999999999959e18, + 0.92312e6, + 0, + 1.268241794562545266e18, + 1e18 + ); + } + } else { + return PriceData( + 0.929402e6, 0, 1.243374308394652239e18, 0.9266e6, 0, 1.254399999999999959e18, 1e18 + ); + } + } else { + if (price < 0.935697e6) { + return PriceData( + 0.935697e6, 0, 1.218994419994757328e18, 0.929402e6, 0, 1.243374308394652239e18, 1e18 + ); + } else { + return PriceData( + 0.94201e6, 0, 1.195092568622310836e18, 0.935697e6, 0, 1.218994419994757328e18, 1e18 + ); + } + } + } + } else { + if (price < 0.96748e6) { + if (price < 0.961075e6) { + if (price < 0.954697e6) { + if (price < 0.948343e6) { + return PriceData( + 0.948343e6, + 0, + 1.171659381002265521e18, + 0.94201e6, + 0, + 1.195092568622310836e18, + 1e18 + ); + } else { + return PriceData( + 0.954697e6, + 0, + 1.14868566764928004e18, + 0.948343e6, + 0, + 1.171659381002265521e18, + 1e18 + ); + } + } else { + return PriceData( + 0.961075e6, 0, 1.12616241926400007e18, 0.954697e6, 0, 1.14868566764928004e18, 1e18 + ); + } + } else { + if (price < 0.962847e6) { + return PriceData( + 0.962847e6, 0, 1.120000000000000107e18, 0.961075e6, 0, 1.12616241926400007e18, 1e18 + ); + } else { + return PriceData( + 0.96748e6, 0, 1.104080803200000016e18, 0.962847e6, 0, 1.120000000000000107e18, 1e18 + ); + } + } + } else { + if (price < 0.986882e6) { + if (price < 0.98038e6) { + if (price < 0.973914e6) { + return PriceData( + 0.973914e6, + 0, + 1.082432159999999977e18, + 0.96748e6, + 0, + 1.104080803200000016e18, + 1e18 + ); + } else { + return PriceData( + 0.98038e6, + 0, + 1.061208000000000151e18, + 0.973914e6, + 0, + 1.082432159999999977e18, + 1e18 + ); + } + } else { + return PriceData( + 0.986882e6, 0, 1.040399999999999991e18, 0.98038e6, 0, 1.061208000000000151e18, 1e18 + ); + } + } else { + if (price < 0.993421e6) { + return PriceData( + 0.993421e6, 0, 1.020000000000000018e18, 0.986882e6, 0, 1.040399999999999991e18, 1e18 + ); + } else { + return PriceData( + 1.006758e6, 0, 0.980000000000000093e18, 0.993421e6, 0, 1.020000000000000018e18, 1e18 + ); + } + } + } + } + } + } else { + if (price < 1.140253e6) { + if (price < 1.077582e6) { + if (price < 1.04366e6) { + if (price < 1.027335e6) { + if (price < 1.020422e6) { + if (price < 1.013564e6) { + return PriceData( + 1.013564e6, + 0, + 0.960400000000000031e18, + 1.006758e6, + 0, + 0.980000000000000093e18, + 1e18 + ); + } else { + return PriceData( + 1.020422e6, + 0, + 0.941192000000000029e18, + 1.013564e6, + 0, + 0.960400000000000031e18, + 1e18 + ); + } + } else { + return PriceData( + 1.027335e6, 0, 0.922368159999999992e18, 1.020422e6, 0, 0.941192000000000029e18, 1e18 + ); + } + } else { + if (price < 1.041342e6) { + if (price < 1.034307e6) { + return PriceData( + 1.034307e6, + 0, + 0.903920796799999926e18, + 1.027335e6, + 0, + 0.922368159999999992e18, + 1e18 + ); + } else { + return PriceData( + 1.041342e6, + 0, + 0.885842380864000023e18, + 1.034307e6, + 0, + 0.903920796799999926e18, + 1e18 + ); + } + } else { + return PriceData( + 1.04366e6, 0, 0.880000000000000004e18, 1.041342e6, 0, 0.885842380864000023e18, 1e18 + ); + } + } + } else { + if (price < 1.062857e6) { + if (price < 1.055613e6) { + if (price < 1.048443e6) { + return PriceData( + 1.048443e6, + 0, + 0.868125533246720038e18, + 1.04366e6, + 0, + 0.880000000000000004e18, + 1e18 + ); + } else { + return PriceData( + 1.055613e6, + 0, + 0.8507630225817856e18, + 1.048443e6, + 0, + 0.868125533246720038e18, + 1e18 + ); + } + } else { + return PriceData( + 1.062857e6, 0, 0.833747762130149894e18, 1.055613e6, 0, 0.8507630225817856e18, 1e18 + ); + } + } else { + if (price < 1.070179e6) { + return PriceData( + 1.070179e6, 0, 0.81707280688754691e18, 1.062857e6, 0, 0.833747762130149894e18, 1e18 + ); + } else { + return PriceData( + 1.077582e6, 0, 0.800731350749795956e18, 1.070179e6, 0, 0.81707280688754691e18, 1e18 + ); + } + } + } + } else { + if (price < 1.108094e6) { + if (price < 1.09265e6) { + if (price < 1.090025e6) { + if (price < 1.085071e6) { + return PriceData( + 1.085071e6, + 0, + 0.784716723734800059e18, + 1.077582e6, + 0, + 0.800731350749795956e18, + 1e18 + ); + } else { + return PriceData( + 1.090025e6, + 0, + 0.774399999999999977e18, + 1.085071e6, + 0, + 0.784716723734800059e18, + 1e18 + ); + } + } else { + return PriceData( + 1.09265e6, 0, 0.769022389260104022e18, 1.090025e6, 0, 0.774399999999999977e18, 1e18 + ); + } + } else { + if (price < 1.100323e6) { + return PriceData( + 1.100323e6, 0, 0.753641941474902044e18, 1.09265e6, 0, 0.769022389260104022e18, 1e18 + ); + } else { + return PriceData( + 1.108094e6, 0, 0.738569102645403985e18, 1.100323e6, 0, 0.753641941474902044e18, 1e18 + ); + } + } + } else { + if (price < 1.132044e6) { + if (price < 1.123949e6) { + if (price < 1.115967e6) { + return PriceData( + 1.115967e6, + 0, + 0.723797720592495919e18, + 1.108094e6, + 0, + 0.738569102645403985e18, + 1e18 + ); + } else { + return PriceData( + 1.123949e6, + 0, + 0.709321766180645907e18, + 1.115967e6, + 0, + 0.723797720592495919e18, + 1e18 + ); + } + } else { + return PriceData( + 1.132044e6, 0, 0.695135330857033051e18, 1.123949e6, 0, 0.709321766180645907e18, 1e18 + ); + } + } else { + if (price < 1.14011e6) { + return PriceData( + 1.14011e6, 0, 0.681471999999999967e18, 1.132044e6, 0, 0.695135330857033051e18, 1e18 + ); + } else { + return PriceData( + 1.140253e6, 0, 0.681232624239892393e18, 1.14011e6, 0, 0.681471999999999967e18, 1e18 + ); + } + } + } + } + } else { + if (price < 2.01775e6) { + if (price < 1.403579e6) { + if (price < 1.256266e6) { + if (price < 1.195079e6) { + if (price < 1.148586e6) { + return PriceData( + 1.148586e6, + 0, + 0.667607971755094454e18, + 1.140253e6, + 0, + 0.681232624239892393e18, + 1e18 + ); + } else { + return PriceData( + 1.195079e6, + 0, + 0.599695360000000011e18, + 1.148586e6, + 0, + 0.667607971755094454e18, + 1e18 + ); + } + } else { + return PriceData( + 1.256266e6, 0, 0.527731916799999978e18, 1.195079e6, 0, 0.599695360000000011e18, 1e18 + ); + } + } else { + if (price < 1.325188e6) { + return PriceData( + 1.325188e6, 0, 0.464404086784000025e18, 1.256266e6, 0, 0.527731916799999978e18, 1e18 + ); + } else { + return PriceData( + 1.403579e6, 0, 0.408675596369920013e18, 1.325188e6, 0, 0.464404086784000025e18, 1e18 + ); + } + } + } else { + if (price < 1.716848e6) { + if (price < 1.596984e6) { + if (price < 1.493424e6) { + return PriceData( + 1.493424e6, + 0, + 0.359634524805529598e18, + 1.403579e6, + 0, + 0.408675596369920013e18, + 1e18 + ); + } else { + return PriceData( + 1.596984e6, + 0, + 0.316478381828866062e18, + 1.493424e6, + 0, + 0.359634524805529598e18, + 1e18 + ); + } + } else { + return PriceData( + 1.716848e6, 0, 0.278500976009402101e18, 1.596984e6, 0, 0.316478381828866062e18, 1e18 + ); + } + } else { + if (price < 1.855977e6) { + return PriceData( + 1.855977e6, 0, 0.245080858888273884e18, 1.716848e6, 0, 0.278500976009402101e18, 1e18 + ); + } else { + return PriceData( + 2.01775e6, 0, 0.215671155821681004e18, 1.855977e6, 0, 0.245080858888273884e18, 1e18 + ); + } + } + } + } else { + if (price < 3.322705e6) { + if (price < 2.680458e6) { + if (price < 2.425256e6) { + if (price < 2.206036e6) { + return PriceData( + 2.206036e6, + 0, + 0.189790617123079292e18, + 2.01775e6, + 0, + 0.215671155821681004e18, + 1e18 + ); + } else { + return PriceData( + 2.425256e6, + 0, + 0.167015743068309769e18, + 2.206036e6, + 0, + 0.189790617123079292e18, + 1e18 + ); + } + } else { + return PriceData( + 2.680458e6, 0, 0.146973853900112583e18, 2.425256e6, 0, 0.167015743068309769e18, 1e18 + ); + } + } else { + if (price < 2.977411e6) { + return PriceData( + 2.977411e6, 0, 0.129336991432099091e18, 2.680458e6, 0, 0.146973853900112583e18, 1e18 + ); + } else { + return PriceData( + 3.322705e6, 0, 0.113816552460247203e18, 2.977411e6, 0, 0.129336991432099091e18, 1e18 + ); + } + } + } else { + if (price < 4.729321e6) { + if (price < 4.189464e6) { + if (price < 3.723858e6) { + return PriceData( + 3.723858e6, + 0, + 0.100158566165017532e18, + 3.322705e6, + 0, + 0.113816552460247203e18, + 1e18 + ); + } else { + return PriceData( + 4.189464e6, + 0, + 0.088139538225215433e18, + 3.723858e6, + 0, + 0.100158566165017532e18, + 1e18 + ); + } + } else { + return PriceData( + 4.729321e6, 0, 0.077562793638189589e18, 4.189464e6, 0, 0.088139538225215433e18, 1e18 + ); + } + } else { + if (price < 10.37089e6) { + return PriceData( + 10.37089e6, 0, 0.035714285714285712e18, 4.729321e6, 0, 0.077562793638189589e18, 1e18 + ); + } else { + revert("LUT: Invalid price"); + } + } + } + } + } + } + } + + /** + * @notice Returns the estimated range of reserve ratios for a given price, + * assuming the pool liquidity remains constant. + */ + function getRatiosFromPriceSwap(uint256 price) external pure returns (PriceData memory) { + if (price < 0.993344e6) { + if (price < 0.834426e6) { + if (price < 0.718073e6) { + if (price < 0.391201e6) { + if (price < 0.264147e6) { + if (price < 0.213318e6) { + if (price < 0.001083e6) { + revert("LUT: Invalid price"); + } else { + return PriceData( + 0.213318e6, + 0.188693329162796575e18, + 2.410556040105746423e18, + 0.001083e6, + 0.005263157894736842e18, + 10.522774272309483479e18, + 1e18 + ); + } + } else { + if (price < 0.237671e6) { + return PriceData( + 0.237671e6, + 0.20510144474217018e18, + 2.337718072004858261e18, + 0.213318e6, + 0.188693329162796575e18, + 2.410556040105746423e18, + 1e18 + ); + } else { + return PriceData( + 0.264147e6, + 0.222936352980619729e18, + 2.26657220303422724e18, + 0.237671e6, + 0.20510144474217018e18, + 2.337718072004858261e18, + 1e18 + ); + } + } + } else { + if (price < 0.323531e6) { + if (price < 0.292771e6) { + return PriceData( + 0.292771e6, + 0.242322122805021467e18, + 2.196897480682568293e18, + 0.264147e6, + 0.222936352980619729e18, + 2.26657220303422724e18, + 1e18 + ); + } else { + return PriceData( + 0.323531e6, + 0.263393611744588529e18, + 2.128468246736633152e18, + 0.292771e6, + 0.242322122805021467e18, + 2.196897480682568293e18, + 1e18 + ); + } + } else { + if (price < 0.356373e6) { + return PriceData( + 0.356373e6, + 0.286297404070204931e18, + 2.061053544007124483e18, + 0.323531e6, + 0.263393611744588529e18, + 2.128468246736633152e18, + 1e18 + ); + } else { + return PriceData( + 0.391201e6, + 0.311192830511092366e18, + 1.994416599735895801e18, + 0.356373e6, + 0.286297404070204931e18, + 2.061053544007124483e18, + 1e18 + ); + } + } + } + } else { + if (price < 0.546918e6) { + if (price < 0.466197e6) { + if (price < 0.427871e6) { + return PriceData( + 0.427871e6, + 0.338253076642491657e18, + 1.92831441898410505e18, + 0.391201e6, + 0.311192830511092366e18, + 1.994416599735895801e18, + 1e18 + ); + } else { + return PriceData( + 0.466197e6, + 0.367666387654882243e18, + 1.86249753363281334e18, + 0.427871e6, + 0.338253076642491657e18, + 1.92831441898410505e18, + 1e18 + ); + } + } else { + if (price < 0.50596e6) { + return PriceData( + 0.50596e6, + 0.399637377885741607e18, + 1.796709969924970451e18, + 0.466197e6, + 0.367666387654882243e18, + 1.86249753363281334e18, + 1e18 + ); + } else { + return PriceData( + 0.546918e6, + 0.434388454223632148e18, + 1.73068952191306602e18, + 0.50596e6, + 0.399637377885741607e18, + 1.796709969924970451e18, + 1e18 + ); + } + } + } else { + if (price < 0.631434e6) { + if (price < 0.588821e6) { + return PriceData( + 0.588821e6, + 0.472161363286556723e18, + 1.664168452923131536e18, + 0.546918e6, + 0.434388454223632148e18, + 1.73068952191306602e18, + 1e18 + ); + } else { + return PriceData( + 0.631434e6, + 0.513218873137561538e18, + 1.596874796852916001e18, + 0.588821e6, + 0.472161363286556723e18, + 1.664168452923131536e18, + 1e18 + ); + } + } else { + if (price < 0.67456e6) { + return PriceData( + 0.67456e6, + 0.55784660123648e18, + 1.52853450260679824e18, + 0.631434e6, + 0.513218873137561538e18, + 1.596874796852916001e18, + 1e18 + ); + } else { + return PriceData( + 0.718073e6, + 0.606355001344e18, + 1.458874768183093584e18, + 0.67456e6, + 0.55784660123648e18, + 1.52853450260679824e18, + 1e18 + ); + } + } + } + } + } else { + if (price < 0.801931e6) { + if (price < 0.780497e6) { + if (price < 0.769833e6) { + if (price < 0.76195e6) { + return PriceData( + 0.76195e6, + 0.659081523200000019e18, + 1.387629060213009469e18, + 0.718073e6, + 0.606355001344e18, + 1.458874768183093584e18, + 1e18 + ); + } else { + return PriceData( + 0.769833e6, + 0.668971758569680497e18, + 1.37471571145172633e18, + 0.76195e6, + 0.659081523200000019e18, + 1.387629060213009469e18, + 1e18 + ); + } + } else { + if (price < 0.775161e6) { + return PriceData( + 0.775161e6, + 0.675729049060283415e18, + 1.365968375000512491e18, + 0.769833e6, + 0.668971758569680497e18, + 1.37471571145172633e18, + 1e18 + ); + } else { + return PriceData( + 0.780497e6, + 0.682554595010387288e18, + 1.357193251389227306e18, + 0.775161e6, + 0.675729049060283415e18, + 1.365968375000512491e18, + 1e18 + ); + } + } + } else { + if (price < 0.791195e6) { + if (price < 0.785842e6) { + return PriceData( + 0.785842e6, + 0.689449085869078049e18, + 1.34838993014876074e18, + 0.780497e6, + 0.682554595010387288e18, + 1.357193251389227306e18, + 1e18 + ); + } else { + return PriceData( + 0.791195e6, + 0.696413218049573679e18, + 1.339558007037547016e18, + 0.785842e6, + 0.689449085869078049e18, + 1.34838993014876074e18, + 1e18 + ); + } + } else { + if (price < 0.796558e6) { + return PriceData( + 0.796558e6, + 0.703447694999569495e18, + 1.330697084427678423e18, + 0.791195e6, + 0.696413218049573679e18, + 1.339558007037547016e18, + 1e18 + ); + } else { + return PriceData( + 0.801931e6, + 0.710553227272292309e18, + 1.321806771708554873e18, + 0.796558e6, + 0.703447694999569495e18, + 1.330697084427678423e18, + 1e18 + ); + } + } + } + } else { + if (price < 0.818119e6) { + if (price < 0.807315e6) { + if (price < 0.806314e6) { + return PriceData( + 0.806314e6, + 0.716392959999999968e18, + 1.314544530202049311e18, + 0.801931e6, + 0.710553227272292309e18, + 1.321806771708554873e18, + 1e18 + ); + } else { + return PriceData( + 0.807315e6, + 0.717730532598275128e18, + 1.312886685708826162e18, + 0.806314e6, + 0.716392959999999968e18, + 1.314544530202049311e18, + 1e18 + ); + } + } else { + if (price < 0.812711e6) { + return PriceData( + 0.812711e6, + 0.724980335957853717e18, + 1.303936451137418295e18, + 0.807315e6, + 0.717730532598275128e18, + 1.312886685708826162e18, + 1e18 + ); + } else { + return PriceData( + 0.818119e6, + 0.732303369654397684e18, + 1.294955701044462559e18, + 0.812711e6, + 0.724980335957853717e18, + 1.303936451137418295e18, + 1e18 + ); + } + } + } else { + if (price < 0.828976e6) { + if (price < 0.82354e6) { + return PriceData( + 0.82354e6, + 0.73970037338828043e18, + 1.285944077302980215e18, + 0.818119e6, + 0.732303369654397684e18, + 1.294955701044462559e18, + 1e18 + ); + } else { + return PriceData( + 0.828976e6, + 0.74717209433159637e18, + 1.276901231112211654e18, + 0.82354e6, + 0.73970037338828043e18, + 1.285944077302980215e18, + 1e18 + ); + } + } else { + return PriceData( + 0.834426e6, + 0.754719287203632794e18, + 1.267826823523503732e18, + 0.828976e6, + 0.74717209433159637e18, + 1.276901231112211654e18, + 1e18 + ); + } + } + } + } + } else { + if (price < 0.907266e6) { + if (price < 0.873109e6) { + if (price < 0.851493e6) { + if (price < 0.845379e6) { + if (price < 0.839894e6) { + return PriceData( + 0.839894e6, + 0.762342714347103767e18, + 1.258720525989716954e18, + 0.834426e6, + 0.754719287203632794e18, + 1.267826823523503732e18, + 1e18 + ); + } else { + return PriceData( + 0.845379e6, + 0.770043145805155316e18, + 1.249582020939133509e18, + 0.839894e6, + 0.762342714347103767e18, + 1.258720525989716954e18, + 1e18 + ); + } + } else { + if (price < 0.850882e6) { + return PriceData( + 0.850882e6, + 0.777821359399146761e18, + 1.240411002374896432e18, + 0.845379e6, + 0.770043145805155316e18, + 1.249582020939133509e18, + 1e18 + ); + } else { + return PriceData( + 0.851493e6, + 0.778688000000000047e18, + 1.239392846883276889e18, + 0.850882e6, + 0.777821359399146761e18, + 1.240411002374896432e18, + 1e18 + ); + } + } + } else { + if (price < 0.86195e6) { + if (price < 0.856405e6) { + return PriceData( + 0.856405e6, + 0.785678140807218983e18, + 1.231207176501035727e18, + 0.851493e6, + 0.778688000000000047e18, + 1.239392846883276889e18, + 1e18 + ); + } else { + return PriceData( + 0.86195e6, + 0.793614283643655494e18, + 1.221970262376178118e18, + 0.856405e6, + 0.785678140807218983e18, + 1.231207176501035727e18, + 1e18 + ); + } + } else { + if (price < 0.867517e6) { + return PriceData( + 0.867517e6, + 0.801630589539045979e18, + 1.212699992596070864e18, + 0.86195e6, + 0.793614283643655494e18, + 1.221970262376178118e18, + 1e18 + ); + } else { + return PriceData( + 0.873109e6, + 0.809727868221258529e18, + 1.203396114006087814e18, + 0.867517e6, + 0.801630589539045979e18, + 1.212699992596070864e18, + 1e18 + ); + } + } + } + } else { + if (price < 0.895753e6) { + if (price < 0.884372e6) { + if (price < 0.878727e6) { + return PriceData( + 0.878727e6, + 0.817906937597230987e18, + 1.194058388444914964e18, + 0.873109e6, + 0.809727868221258529e18, + 1.203396114006087814e18, + 1e18 + ); + } else { + return PriceData( + 0.884372e6, + 0.826168623835586646e18, + 1.18468659352065786e18, + 0.878727e6, + 0.817906937597230987e18, + 1.194058388444914964e18, + 1e18 + ); + } + } else { + if (price < 0.890047e6) { + return PriceData( + 0.890047e6, + 0.834513761450087599e18, + 1.17528052342063094e18, + 0.884372e6, + 0.826168623835586646e18, + 1.18468659352065786e18, + 1e18 + ); + } else { + return PriceData( + 0.895753e6, + 0.84294319338392687e18, + 1.16583998975613734e18, + 0.890047e6, + 0.834513761450087599e18, + 1.17528052342063094e18, + 1e18 + ); + } + } + } else { + if (price < 0.901491e6) { + if (price < 0.898085e6) { + return PriceData( + 0.898085e6, + 0.846400000000000041e18, + 1.161985895520041945e18, + 0.895753e6, + 0.84294319338392687e18, + 1.16583998975613734e18, + 1e18 + ); + } else { + return PriceData( + 0.901491e6, + 0.851457771094875637e18, + 1.156364822443562979e18, + 0.898085e6, + 0.846400000000000041e18, + 1.161985895520041945e18, + 1e18 + ); + } + } else { + return PriceData( + 0.907266e6, + 0.860058354641288547e18, + 1.146854870623147615e18, + 0.901491e6, + 0.851457771094875637e18, + 1.156364822443562979e18, + 1e18 + ); + } + } + } + } else { + if (price < 0.948888e6) { + if (price < 0.930767e6) { + if (price < 0.918932e6) { + if (price < 0.913079e6) { + return PriceData( + 0.913079e6, + 0.868745812768978332e18, + 1.137310003616810228e18, + 0.907266e6, + 0.860058354641288547e18, + 1.146854870623147615e18, + 1e18 + ); + } else { + return PriceData( + 0.918932e6, + 0.877521022998967948e18, + 1.127730111926438461e18, + 0.913079e6, + 0.868745812768978332e18, + 1.137310003616810228e18, + 1e18 + ); + } + } else { + if (price < 0.924827e6) { + return PriceData( + 0.924827e6, + 0.88638487171612923e18, + 1.118115108274055913e18, + 0.918932e6, + 0.877521022998967948e18, + 1.127730111926438461e18, + 1e18 + ); + } else { + return PriceData( + 0.930767e6, + 0.895338254258716493e18, + 1.10846492868530544e18, + 0.924827e6, + 0.88638487171612923e18, + 1.118115108274055913e18, + 1e18 + ); + } + } + } else { + if (price < 0.942795e6) { + if (price < 0.936756e6) { + return PriceData( + 0.936756e6, + 0.90438207500880452e18, + 1.09877953361768621e18, + 0.930767e6, + 0.895338254258716493e18, + 1.10846492868530544e18, + 1e18 + ); + } else { + return PriceData( + 0.942795e6, + 0.913517247483640937e18, + 1.089058909134983155e18, + 0.936756e6, + 0.90438207500880452e18, + 1.09877953361768621e18, + 1e18 + ); + } + } else { + if (price < 0.947076e6) { + return PriceData( + 0.947076e6, + 0.92000000000000004e18, + 1.082198372170484424e18, + 0.942795e6, + 0.913517247483640937e18, + 1.089058909134983155e18, + 1e18 + ); + } else { + return PriceData( + 0.948888e6, + 0.922744694427920065e18, + 1.079303068129318754e18, + 0.947076e6, + 0.92000000000000004e18, + 1.082198372170484424e18, + 1e18 + ); + } + } + } + } else { + if (price < 0.973868e6) { + if (price < 0.961249e6) { + if (price < 0.955039e6) { + return PriceData( + 0.955039e6, + 0.932065347906990027e18, + 1.069512051592246715e18, + 0.948888e6, + 0.922744694427920065e18, + 1.079303068129318754e18, + 1e18 + ); + } else { + return PriceData( + 0.961249e6, + 0.941480149400999999e18, + 1.059685929936267312e18, + 0.955039e6, + 0.932065347906990027e18, + 1.069512051592246715e18, + 1e18 + ); + } + } else { + if (price < 0.967525e6) { + return PriceData( + 0.967525e6, + 0.950990049900000023e18, + 1.049824804368118425e18, + 0.961249e6, + 0.941480149400999999e18, + 1.059685929936267312e18, + 1e18 + ); + } else { + return PriceData( + 0.973868e6, + 0.960596010000000056e18, + 1.039928808315135234e18, + 0.967525e6, + 0.950990049900000023e18, + 1.049824804368118425e18, + 1e18 + ); + } + } + } else { + if (price < 0.986773e6) { + if (price < 0.980283e6) { + return PriceData( + 0.980283e6, + 0.970299000000000134e18, + 1.029998108905910481e18, + 0.973868e6, + 0.960596010000000056e18, + 1.039928808315135234e18, + 1e18 + ); + } else { + return PriceData( + 0.986773e6, + 0.980099999999999971e18, + 1.020032908506394831e18, + 0.980283e6, + 0.970299000000000134e18, + 1.029998108905910481e18, + 1e18 + ); + } + } else { + return PriceData( + 0.993344e6, + 0.989999999999999991e18, + 1.01003344631248293e18, + 0.986773e6, + 0.980099999999999971e18, + 1.020032908506394831e18, + 1e18 + ); + } + } + } + } + } + } else { + if (price < 1.211166e6) { + if (price < 1.09577e6) { + if (price < 1.048893e6) { + if (price < 1.027293e6) { + if (price < 1.01345e6) { + if (price < 1.006679e6) { + return PriceData( + 1.006679e6, + 1.010000000000000009e18, + 0.990033224058159078e18, + 0.993344e6, + 0.989999999999999991e18, + 1.01003344631248293e18, + 1e18 + ); + } else { + return PriceData( + 1.01345e6, + 1.020100000000000007e18, + 0.980033797419900599e18, + 1.006679e6, + 1.010000000000000009e18, + 0.990033224058159078e18, + 1e18 + ); + } + } else { + if (price < 1.020319e6) { + return PriceData( + 1.020319e6, + 1.030300999999999911e18, + 0.970002111104709575e18, + 1.01345e6, + 1.020100000000000007e18, + 0.980033797419900599e18, + 1e18 + ); + } else { + return PriceData( + 1.027293e6, + 1.040604010000000024e18, + 0.959938599971011053e18, + 1.020319e6, + 1.030300999999999911e18, + 0.970002111104709575e18, + 1e18 + ); + } + } + } else { + if (price < 1.034375e6) { + if (price < 1.033686e6) { + return PriceData( + 1.033686e6, + 1.050000000000000044e18, + 0.950820553711780869e18, + 1.027293e6, + 1.040604010000000024e18, + 0.959938599971011053e18, + 1e18 + ); + } else { + return PriceData( + 1.034375e6, + 1.051010050100000148e18, + 0.949843744564435544e18, + 1.033686e6, + 1.050000000000000044e18, + 0.950820553711780869e18, + 1e18 + ); + } + } else { + if (price < 1.041574e6) { + return PriceData( + 1.041574e6, + 1.061520150601000134e18, + 0.93971807302139454e18, + 1.034375e6, + 1.051010050100000148e18, + 0.949843744564435544e18, + 1e18 + ); + } else { + return PriceData( + 1.048893e6, + 1.072135352107010053e18, + 0.929562163027227939e18, + 1.041574e6, + 1.061520150601000134e18, + 0.93971807302139454e18, + 1e18 + ); + } + } + } + } else { + if (price < 1.071652e6) { + if (price < 1.063925e6) { + if (price < 1.056342e6) { + return PriceData( + 1.056342e6, + 1.082856705628080007e18, + 0.919376643827810258e18, + 1.048893e6, + 1.072135352107010053e18, + 0.929562163027227939e18, + 1e18 + ); + } else { + return PriceData( + 1.063925e6, + 1.093685272684360887e18, + 0.90916219829307332e18, + 1.056342e6, + 1.082856705628080007e18, + 0.919376643827810258e18, + 1e18 + ); + } + } else { + if (price < 1.070147e6) { + return PriceData( + 1.070147e6, + 1.102500000000000036e18, + 0.900901195775543062e18, + 1.063925e6, + 1.093685272684360887e18, + 0.90916219829307332e18, + 1e18 + ); + } else { + return PriceData( + 1.071652e6, + 1.104622125411204525e18, + 0.89891956503043724e18, + 1.070147e6, + 1.102500000000000036e18, + 0.900901195775543062e18, + 1e18 + ); + } + } + } else { + if (price < 1.087566e6) { + if (price < 1.079529e6) { + return PriceData( + 1.079529e6, + 1.115668346665316557e18, + 0.888649540545595529e18, + 1.071652e6, + 1.104622125411204525e18, + 0.89891956503043724e18, + 1e18 + ); + } else { + return PriceData( + 1.087566e6, + 1.126825030131969774e18, + 0.878352981447521719e18, + 1.079529e6, + 1.115668346665316557e18, + 0.888649540545595529e18, + 1e18 + ); + } + } else { + return PriceData( + 1.09577e6, + 1.1380932804332895e18, + 0.868030806693890433e18, + 1.087566e6, + 1.126825030131969774e18, + 0.878352981447521719e18, + 1e18 + ); + } + } + } + } else { + if (price < 1.15496e6) { + if (price < 1.121482e6) { + if (price < 1.110215e6) { + if (price < 1.104151e6) { + return PriceData( + 1.104151e6, + 1.149474213237622333e18, + 0.857683999872391523e18, + 1.09577e6, + 1.1380932804332895e18, + 0.868030806693890433e18, + 1e18 + ); + } else { + return PriceData( + 1.110215e6, + 1.157625000000000126e18, + 0.850322213751246947e18, + 1.104151e6, + 1.149474213237622333e18, + 0.857683999872391523e18, + 1e18 + ); + } + } else { + if (price < 1.112718e6) { + return PriceData( + 1.112718e6, + 1.160968955369998667e18, + 0.847313611512600207e18, + 1.110215e6, + 1.157625000000000126e18, + 0.850322213751246947e18, + 1e18 + ); + } else { + return PriceData( + 1.121482e6, + 1.172578644923698565e18, + 0.836920761422192294e18, + 1.112718e6, + 1.160968955369998667e18, + 0.847313611512600207e18, + 1e18 + ); + } + } + } else { + if (price < 1.139642e6) { + if (price < 1.130452e6) { + return PriceData( + 1.130452e6, + 1.184304431372935618e18, + 0.826506641040327228e18, + 1.121482e6, + 1.172578644923698565e18, + 0.836920761422192294e18, + 1e18 + ); + } else { + return PriceData( + 1.139642e6, + 1.196147475686665018e18, + 0.8160725157999702e18, + 1.130452e6, + 1.184304431372935618e18, + 0.826506641040327228e18, + 1e18 + ); + } + } else { + if (price < 1.149062e6) { + return PriceData( + 1.149062e6, + 1.208108950443531393e18, + 0.805619727489791271e18, + 1.139642e6, + 1.196147475686665018e18, + 0.8160725157999702e18, + 1e18 + ); + } else { + return PriceData( + 1.15496e6, + 1.21550625000000001e18, + 0.799198479643147719e18, + 1.149062e6, + 1.208108950443531393e18, + 0.805619727489791271e18, + 1e18 + ); + } + } + } + } else { + if (price < 1.189304e6) { + if (price < 1.168643e6) { + if (price < 1.158725e6) { + return PriceData( + 1.158725e6, + 1.22019003994796682e18, + 0.795149696605042422e18, + 1.15496e6, + 1.21550625000000001e18, + 0.799198479643147719e18, + 1e18 + ); + } else { + return PriceData( + 1.168643e6, + 1.232391940347446369e18, + 0.784663924675502389e18, + 1.158725e6, + 1.22019003994796682e18, + 0.795149696605042422e18, + 1e18 + ); + } + } else { + if (price < 1.178832e6) { + return PriceData( + 1.178832e6, + 1.244715859750920917e18, + 0.774163996557160172e18, + 1.168643e6, + 1.232391940347446369e18, + 0.784663924675502389e18, + 1e18 + ); + } else { + return PriceData( + 1.189304e6, + 1.257163018348430139e18, + 0.763651582672810969e18, + 1.178832e6, + 1.244715859750920917e18, + 0.774163996557160172e18, + 1e18 + ); + } + } + } else { + if (price < 1.205768e6) { + if (price < 1.200076e6) { + return PriceData( + 1.200076e6, + 1.269734648531914534e18, + 0.753128441185147435e18, + 1.189304e6, + 1.257163018348430139e18, + 0.763651582672810969e18, + 1e18 + ); + } else { + return PriceData( + 1.205768e6, + 1.276281562499999911e18, + 0.747685899578659385e18, + 1.200076e6, + 1.269734648531914534e18, + 0.753128441185147435e18, + 1e18 + ); + } + } else { + return PriceData( + 1.211166e6, + 1.282431995017233595e18, + 0.74259642008426785e18, + 1.205768e6, + 1.276281562499999911e18, + 0.747685899578659385e18, + 1e18 + ); + } + } + } + } + } else { + if (price < 1.393403e6) { + if (price < 1.299217e6) { + if (price < 1.259043e6) { + if (price < 1.234362e6) { + if (price < 1.222589e6) { + return PriceData( + 1.222589e6, + 1.295256314967406119e18, + 0.732057459169776381e18, + 1.211166e6, + 1.282431995017233595e18, + 0.74259642008426785e18, + 1e18 + ); + } else { + return PriceData( + 1.234362e6, + 1.308208878117080198e18, + 0.721513591905860174e18, + 1.222589e6, + 1.295256314967406119e18, + 0.732057459169776381e18, + 1e18 + ); + } + } else { + if (price < 1.246507e6) { + return PriceData( + 1.246507e6, + 1.321290966898250874e18, + 0.710966947125877935e18, + 1.234362e6, + 1.308208878117080198e18, + 0.721513591905860174e18, + 1e18 + ); + } else { + return PriceData( + 1.259043e6, + 1.33450387656723346e18, + 0.700419750561125598e18, + 1.246507e6, + 1.321290966898250874e18, + 0.710966947125877935e18, + 1e18 + ); + } + } + } else { + if (price < 1.271991e6) { + if (price < 1.264433e6) { + return PriceData( + 1.264433e6, + 1.340095640624999973e18, + 0.695987932996588454e18, + 1.259043e6, + 1.33450387656723346e18, + 0.700419750561125598e18, + 1e18 + ); + } else { + return PriceData( + 1.271991e6, + 1.347848915332905628e18, + 0.689874326166576179e18, + 1.264433e6, + 1.340095640624999973e18, + 0.695987932996588454e18, + 1e18 + ); + } + } else { + if (price < 1.285375e6) { + return PriceData( + 1.285375e6, + 1.361327404486234682e18, + 0.67933309721453039e18, + 1.271991e6, + 1.347848915332905628e18, + 0.689874326166576179e18, + 1e18 + ); + } else { + return PriceData( + 1.299217e6, + 1.374940678531097138e18, + 0.668798587125333244e18, + 1.285375e6, + 1.361327404486234682e18, + 0.67933309721453039e18, + 1e18 + ); + } + } + } + } else { + if (price < 1.343751e6) { + if (price < 1.328377e6) { + if (price < 1.313542e6) { + return PriceData( + 1.313542e6, + 1.38869008531640814e18, + 0.658273420002602916e18, + 1.299217e6, + 1.374940678531097138e18, + 0.668798587125333244e18, + 1e18 + ); + } else { + return PriceData( + 1.328377e6, + 1.402576986169572049e18, + 0.647760320838866033e18, + 1.313542e6, + 1.38869008531640814e18, + 0.658273420002602916e18, + 1e18 + ); + } + } else { + if (price < 1.333292e6) { + return PriceData( + 1.333292e6, + 1.407100422656250016e18, + 0.644361360672887962e18, + 1.328377e6, + 1.402576986169572049e18, + 0.647760320838866033e18, + 1e18 + ); + } else { + return PriceData( + 1.343751e6, + 1.416602756031267951e18, + 0.637262115356114656e18, + 1.333292e6, + 1.407100422656250016e18, + 0.644361360672887962e18, + 1e18 + ); + } + } + } else { + if (price < 1.376232e6) { + if (price < 1.359692e6) { + return PriceData( + 1.359692e6, + 1.430768783591580551e18, + 0.626781729444674585e18, + 1.343751e6, + 1.416602756031267951e18, + 0.637262115356114656e18, + 1e18 + ); + } else { + return PriceData( + 1.376232e6, + 1.445076471427496179e18, + 0.616322188162944262e18, + 1.359692e6, + 1.430768783591580551e18, + 0.626781729444674585e18, + 1e18 + ); + } + } else { + return PriceData( + 1.393403e6, + 1.459527236141771489e18, + 0.605886614260108591e18, + 1.376232e6, + 1.445076471427496179e18, + 0.616322188162944262e18, + 1e18 + ); + } + } + } + } else { + if (price < 2.209802e6) { + if (price < 1.514667e6) { + if (price < 1.415386e6) { + if (price < 1.41124e6) { + return PriceData( + 1.41124e6, + 1.474122508503188822e18, + 0.595478226183906334e18, + 1.393403e6, + 1.459527236141771489e18, + 0.605886614260108591e18, + 1e18 + ); + } else { + return PriceData( + 1.415386e6, + 1.47745544378906235e18, + 0.593119977480511928e18, + 1.41124e6, + 1.474122508503188822e18, + 0.595478226183906334e18, + 1e18 + ); + } + } else { + if (price < 1.42978e6) { + return PriceData( + 1.42978e6, + 1.488863733588220883e18, + 0.585100335536025584e18, + 1.415386e6, + 1.47745544378906235e18, + 0.593119977480511928e18, + 1e18 + ); + } else { + return PriceData( + 1.514667e6, + 1.551328215978515557e18, + 0.54263432113736132e18, + 1.42978e6, + 1.488863733588220883e18, + 0.585100335536025584e18, + 1e18 + ); + } + } + } else { + if (price < 1.786708e6) { + if (price < 1.636249e6) { + return PriceData( + 1.636249e6, + 1.628894626777441568e18, + 0.493325115988533236e18, + 1.514667e6, + 1.551328215978515557e18, + 0.54263432113736132e18, + 1e18 + ); + } else { + return PriceData( + 1.786708e6, + 1.710339358116313546e18, + 0.445648172809785581e18, + 1.636249e6, + 1.628894626777441568e18, + 0.493325115988533236e18, + 1e18 + ); + } + } else { + if (price < 1.974398e6) { + return PriceData( + 1.974398e6, + 1.79585632602212919e18, + 0.400069510798421513e18, + 1.786708e6, + 1.710339358116313546e18, + 0.445648172809785581e18, + 1e18 + ); + } else { + return PriceData( + 2.209802e6, + 1.885649142323235772e18, + 0.357031765135700119e18, + 1.974398e6, + 1.79585632602212919e18, + 0.400069510798421513e18, + 1e18 + ); + } + } + } + } else { + if (price < 3.931396e6) { + if (price < 2.878327e6) { + if (price < 2.505865e6) { + return PriceData( + 2.505865e6, + 1.97993159943939756e18, + 0.316916199929126341e18, + 2.209802e6, + 1.885649142323235772e18, + 0.357031765135700119e18, + 1e18 + ); + } else { + return PriceData( + 2.878327e6, + 2.078928179411367427e18, + 0.28000760254479623e18, + 2.505865e6, + 1.97993159943939756e18, + 0.316916199929126341e18, + 1e18 + ); + } + } else { + if (price < 3.346057e6) { + return PriceData( + 3.346057e6, + 2.182874588381935599e18, + 0.246470170347584949e18, + 2.878327e6, + 2.078928179411367427e18, + 0.28000760254479623e18, + 1e18 + ); + } else { + return PriceData( + 3.931396e6, + 2.292018317801032268e18, + 0.216340086006769544e18, + 3.346057e6, + 2.182874588381935599e18, + 0.246470170347584949e18, + 1e18 + ); + } + } + } else { + if (price < 10.709509e6) { + if (price < 4.660591e6) { + return PriceData( + 4.660591e6, + 2.406619233691083881e18, + 0.189535571483960663e18, + 3.931396e6, + 2.292018317801032268e18, + 0.216340086006769544e18, + 1e18 + ); + } else { + return PriceData( + 10.709509e6, + 3e18, + 0.103912563829966526e18, + 4.660591e6, + 2.406619233691083881e18, + 0.189535571483960663e18, + 1e18 + ); + } + } else { + revert("LUT: Invalid price"); + } + } + } + } + } + } + } +} diff --git a/src/interfaces/ILookupTable.sol b/src/interfaces/ILookupTable.sol new file mode 100644 index 00000000..6d774852 --- /dev/null +++ b/src/interfaces/ILookupTable.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +/** + * @title PriceReserveMapping + * @author DeadmanWalking + * @notice In order to reasonably use `calcReserveAtRatioSwap` and `calcReserveAtRatioLiquidity` on chain, + * a lookup table contract is used to decrease the amount of iterations needed to converge into an answer. + */ +interface ILookupTable { + /** + * @notice the lookup table returns a series of data, given a price point: + * @param highPrice the closest price to the targetPrice, where targetPrice < highPrice. + * @param highPriceI reserve i such that `calcRate(reserve, i, j, data)` == highPrice. + * @param highPriceJ reserve j such that `calcRate(reserve, i, j, data)` == highPrice. + * @param lowPrice the closest price to the targetPrice, where targetPrice > lowPrice. + * @param lowPriceI reserve i such that `calcRate(reserve, i, j, data)` == lowPrice. + * @param lowPriceJ reserve j such that `calcRate(reserve, i, j, data)` == lowPrice. + * @param precision the initial reserve values. Assumes the inital reserve i == reserve j + */ + struct PriceData { + uint256 highPrice; + uint256 highPriceI; + uint256 highPriceJ; + uint256 lowPrice; + uint256 lowPriceI; + uint256 lowPriceJ; + uint256 precision; + } + + function getRatiosFromPriceLiquidity(uint256) external view returns (PriceData memory); + function getRatiosFromPriceSwap(uint256) external view returns (PriceData memory); + function getAParameter() external view returns (uint256); +} 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/src/libraries/LibWellUpgradeableConstructor.sol b/src/libraries/LibWellUpgradeableConstructor.sol new file mode 100644 index 00000000..367bc14c --- /dev/null +++ b/src/libraries/LibWellUpgradeableConstructor.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// forgefmt: disable-start + +pragma solidity ^0.8.20; + +import {LibContractInfo} from "src/libraries/LibContractInfo.sol"; +import {Call, IERC20} from "src/Well.sol"; +import {WellUpgradeable} from "src/WellUpgradeable.sol"; + +library LibWellUpgradeableConstructor { + + /** + * @notice Encode the Well's immutable data. + */ + function encodeWellDeploymentData( + address _aquifer, + IERC20[] memory _tokens, + Call memory _wellFunction, + Call[] memory _pumps + ) internal pure returns (bytes memory immutableData, bytes memory initData) { + immutableData = encodeWellImmutableData(_aquifer, _tokens, _wellFunction, _pumps); + initData = abi.encodeWithSelector(WellUpgradeable.initNoWellToken.selector); + } + + /** + * @notice Encode the Well's immutable data. + * @param _aquifer The address of the Aquifer which will deploy this Well. + * @param _tokens A list of ERC20 tokens supported by the Well. + * @param _wellFunction A single Call struct representing a call to the Well Function. + * @param _pumps An array of Call structs representings calls to Pumps. + * @dev `immutableData` is tightly packed, however since `_tokens` itself is + * an array, each address in the array will be padded up to 32 bytes. + * + * Arbitrary-length bytes are applied to the end of the encoded bytes array + * for easy reading of statically-sized data. + * + */ + function encodeWellImmutableData( + address _aquifer, + IERC20[] memory _tokens, + Call memory _wellFunction, + Call[] memory _pumps + ) internal pure returns (bytes memory immutableData) { + + immutableData = abi.encodePacked( + _aquifer, // aquifer address + _tokens.length, // number of tokens + _wellFunction.target, // well function address + _wellFunction.data.length, // well function data length + _pumps.length, // number of pumps + _tokens, // tokens array + _wellFunction.data // well function data (bytes) + ); + for (uint256 i; i < _pumps.length; ++i) { + immutableData = abi.encodePacked( + immutableData, // previously packed pumps + _pumps[i].target, // pump address + _pumps[i].data.length, // pump data length + _pumps[i].data // pump data (bytes) + ); + } + } + + function encodeWellInitFunctionCall( + IERC20[] memory _tokens, + Call memory _wellFunction + ) public view returns (bytes memory initFunctionCall) { + string memory name = LibContractInfo.getSymbol(address(_tokens[0])); + string memory symbol = name; + for (uint256 i = 1; i < _tokens.length; ++i) { + name = string.concat(name, ":", LibContractInfo.getSymbol(address(_tokens[i]))); + symbol = string.concat(symbol, LibContractInfo.getSymbol(address(_tokens[i]))); + } + name = string.concat(name, " ", LibContractInfo.getName(_wellFunction.target), " Upgradeable Well"); + symbol = string.concat(symbol, LibContractInfo.getSymbol(_wellFunction.target), "uw"); + + // See {Well.init}. + initFunctionCall = abi.encodeWithSelector(WellUpgradeable.init.selector, name, symbol); + } + + /** + * @notice Encode a Call struct representing an arbitrary call to `target` with additional data `data`. + */ + function encodeCall(address target, bytes memory data) public pure returns (Call memory) { + return Call(target, data); + } +} 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/Stable2/LookupTable.t.sol b/test/Stable2/LookupTable.t.sol new file mode 100644 index 00000000..8e86c66f --- /dev/null +++ b/test/Stable2/LookupTable.t.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +// forgefmt: disable-start + +pragma solidity ^0.8.20; + +import {TestHelper, Well, IERC20, console} from "test/TestHelper.sol"; +import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; + +contract LookupTableTest is TestHelper { + + Stable2LUT1 lookupTable; + Stable2LUT1.PriceData pd; + + function setUp() public { + lookupTable = new Stable2LUT1(); + } + + function test_getAParameter() public view { + uint256 a = lookupTable.getAParameter(); + assertEq(a , 100); + } + + //////////////// getRatiosFromPriceSwap //////////////// + + function test_getRatiosFromPriceSwapAroundDollarHigh() public { + uint256 currentPrice = 1e6; + // test 1.0 - 1.10 range + for (uint256 i; i<10 ; i++) { + pd = lookupTable.getRatiosFromPriceSwap(currentPrice); + uint256 diff = pd.highPrice - pd.lowPrice; + // 2 cent precision around the dollar mark + assertLt(diff, 0.02e6); + currentPrice += 0.01e6; + } + } + + function test_getRatiosFromPriceSwapAroundDollarLow() public { + uint256 currentPrice = 0.9e6; + // test 0.9 - 1.0 range + for (uint256 i; i<10 ; i++) { + pd = lookupTable.getRatiosFromPriceSwap(currentPrice); + uint256 diff = pd.highPrice - pd.lowPrice; + // 2 cent precision around the dollar mark + assertLt(diff, 0.02e6); + currentPrice += 0.02e6; + } + } + + function test_getRatiosFromPriceSwapExtremeLow() public { + // pick a value close to the min (P~=0.01) + uint256 currentPrice = 0.015e6; + pd = lookupTable.getRatiosFromPriceSwap(currentPrice); + } + + function test_getRatiosFromPriceSwapExtremeHigh() public { + // pick a value close to the max (P~=10) + uint256 currentPrice = 9.84e6; + pd = lookupTable.getRatiosFromPriceSwap(currentPrice); + } + + function testFail_getRatiosFromPriceSwapExtremeLow() public { + // pick an out of bounds value (P<0.01) + uint256 currentPrice = 0.0001e6; + pd = lookupTable.getRatiosFromPriceSwap(currentPrice); + // assert no revert + assert(pd.highPrice > pd.lowPrice); + } + + function testFail_getRatiosFromPriceSwapExtremeHigh() public { + // pick an out of bounds value (P>10) + uint256 currentPrice = 100e6; + pd = lookupTable.getRatiosFromPriceSwap(currentPrice); + // assert no revert + assert(pd.highPrice > pd.lowPrice); + } + + //////////////// getRatiosFromPriceLiquidity //////////////// + + + function test_getRatiosFromPriceLiquidityAroundDollarHigh() public { + uint256 currentPrice = 1e6; + // test 1.0 - 1.10 range + for (uint256 i; i<10 ; i++) { + pd = lookupTable.getRatiosFromPriceLiquidity(currentPrice); + uint256 diff = pd.highPrice - pd.lowPrice; + // 2 cent precision around the dollar mark + assertLt(diff, 0.02e6); + currentPrice += 0.01e6; + } + } + + function test_getRatiosFromPriceLiquidityAroundDollarLow() public { + uint256 currentPrice = 0.9e6; + // test 0.9 - 1.0 range + for (uint256 i; i<10 ; i++) { + pd = lookupTable.getRatiosFromPriceLiquidity(currentPrice); + uint256 diff = pd.highPrice - pd.lowPrice; + // 2 cent precision around the dollar mark + assertLt(diff, 0.02e6); + currentPrice += 0.01e6; + } + } + + function test_getRatiosFromPriceLiquidityExtremeLow() public { + // pick a value close to the min (P=~0.01) + uint256 currentPrice = 0.015e6; + pd = lookupTable.getRatiosFromPriceLiquidity(currentPrice); + // assert no revert + assert(pd.highPrice > pd.lowPrice); + } + + function test_getRatiosFromPriceLiquidityExtremeHigh() public { + // pick a value close to the max (P~=10) + uint256 currentPrice = 9.91e6; + pd = lookupTable.getRatiosFromPriceLiquidity(currentPrice); + // assert no revert + assert(pd.highPrice > pd.lowPrice); + } + + function testFail_getRatiosFromPriceLiquidityExtremeLow() public { + // pick an out of bounds value (P<0.01) + uint256 currentPrice = 0.00001e6; + pd = lookupTable.getRatiosFromPriceLiquidity(currentPrice); + } + + function testFail_getRatiosFromPriceLiquidityExtremeHigh() public { + // pick an out of bounds value (P>10) + uint256 currentPrice = 100e6; + pd = lookupTable.getRatiosFromPriceLiquidity(currentPrice); + } + + ////////////////// Price Range Tests ////////////////// + + function test_PriceRangeSwap() public { + // test range 0.5 - 2.5 + uint256 currentPrice = 0.5e6; + for (uint256 i; i<200 ; i++) { + pd = lookupTable.getRatiosFromPriceSwap(currentPrice); + assertGe(pd.highPrice, currentPrice); + assertLt(pd.lowPrice, currentPrice); + currentPrice += 0.01e6; + } + } + + function test_PriceRangeLiq() public { + // test range 0.5 - 2.5 + uint256 currentPrice = 0.5e6; + for (uint256 i; i<200 ; i++) { + pd = lookupTable.getRatiosFromPriceLiquidity(currentPrice); + assertGe(pd.highPrice, currentPrice); + assertLt(pd.lowPrice, currentPrice); + currentPrice += 0.01e6; + } + } +} diff --git a/test/Stable2/Well.Stable2.AddLiquidity.t.sol b/test/Stable2/Well.Stable2.AddLiquidity.t.sol new file mode 100644 index 00000000..282bf692 --- /dev/null +++ b/test/Stable2/Well.Stable2.AddLiquidity.t.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import {TestHelper, IERC20, Call, Balances} from "test/TestHelper.sol"; +import {ConstantProduct2} from "src/functions/ConstantProduct2.sol"; +import {Snapshot, AddLiquidityAction, RemoveLiquidityAction, LiquidityHelper} from "test/LiquidityHelper.sol"; +import {Math} from "oz/utils/math/Math.sol"; + +contract WellStable2AddLiquidityTest is LiquidityHelper { + function setUp() public { + setupStable2Well(); + } + + /// @dev Liquidity is initially added in {TestHelper}; ensure that subsequent + /// tests will run correctly. + function test_liquidityInitialized() public view { + IERC20[] memory tokens = well.tokens(); + Balances memory userBalance = getBalances(user, well); + Balances memory wellBalance = getBalances(address(well), well); + for (uint256 i; i < tokens.length; i++) { + assertEq(userBalance.tokens[i], initialLiquidity, "incorrect user token reserve"); + assertEq(wellBalance.tokens[i], initialLiquidity, "incorrect well token reserve"); + } + } + + /// @dev Adding liquidity in equal proportions should summate and be scaled + /// up by sqrt(ConstantProduct2.EXP_PRECISION) + function test_getAddLiquidityOut_equalAmounts() public view { + uint256[] memory amounts = new uint256[](tokens.length); + for (uint256 i; i < tokens.length; i++) { + amounts[i] = 1000 * 1e18; + } + uint256 lpAmountOut = well.getAddLiquidityOut(amounts); + assertEq(lpAmountOut, well.totalSupply(), "Incorrect AmountOut"); + } + + function test_getAddLiquidityOut_oneToken() public view { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 10 * 1e18; + amounts[1] = 0; + + uint256 amountOut = well.getAddLiquidityOut(amounts); + assertEq(amountOut, 9_991_708_006_311_592_653, "incorrect amt out"); + } + + function test_addLiquidity_revertIf_minAmountOutTooHigh() public prank(user) { + uint256[] memory amounts = new uint256[](tokens.length); + for (uint256 i; i < tokens.length; i++) { + amounts[i] = 1000 * 1e18; + } + uint256 lpAmountOut = well.getAddLiquidityOut(amounts); + + vm.expectRevert(abi.encodeWithSelector(SlippageOut.selector, lpAmountOut, lpAmountOut + 1)); + well.addLiquidity(amounts, lpAmountOut + 1, user, type(uint256).max); // lpAmountOut is 2000*1e27 + } + + function test_addLiquidity_revertIf_expired() public { + vm.expectRevert(Expired.selector); + well.addLiquidity(new uint256[](tokens.length), 0, user, block.timestamp - 1); + } + + function test_addLiquidity_balanced() public prank(user) { + uint256[] memory amounts = new uint256[](tokens.length); + for (uint256 i; i < tokens.length; i++) { + amounts[i] = 1000 * 1e18; + } + uint256 lpAmountOut = 2000 * 1e18; + + vm.expectEmit(true, true, true, true); + emit AddLiquidity(amounts, lpAmountOut, user); + well.addLiquidity(amounts, lpAmountOut, user, type(uint256).max); + + Balances memory userBalance = getBalances(user, well); + Balances memory wellBalance = getBalances(address(well), well); + + assertEq(userBalance.lp, lpAmountOut); + + // Consumes all of user's tokens + assertEq(userBalance.tokens[0], 0, "incorrect token0 user amt"); + assertEq(userBalance.tokens[1], 0, "incorrect token1 user amt"); + + // Adds to the Well's reserves + assertEq(wellBalance.tokens[0], initialLiquidity + amounts[0], "incorrect token0 well amt"); + assertEq(wellBalance.tokens[1], initialLiquidity + amounts[1], "incorrect token1 well amt"); + checkInvariant(address(well)); + } + + function test_addLiquidity_oneSided() public prank(user) { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 10 * 1e18; + amounts[1] = 0; + + Snapshot memory before; + AddLiquidityAction memory action; + action.amounts = amounts; + action.lpAmountOut = well.getAddLiquidityOut(amounts); + action.recipient = user; + action.fees = new uint256[](2); + + (before, action) = beforeAddLiquidity(action); + well.addLiquidity(amounts, well.getAddLiquidityOut(amounts), user, type(uint256).max); + afterAddLiquidity(before, action); + checkInvariant(address(well)); + } + + /// @dev Adding and removing liquidity in sequence should return the Well to its previous state + function test_addAndRemoveLiquidity() public prank(user) { + uint256[] memory amounts = new uint256[](tokens.length); + for (uint256 i; i < tokens.length; i++) { + amounts[i] = 1000 * 1e18; + } + uint256 lpAmountOut = 2000 * 1e18; + + Snapshot memory before; + AddLiquidityAction memory action; + action.amounts = amounts; + action.lpAmountOut = well.getAddLiquidityOut(amounts); + action.recipient = user; + action.fees = new uint256[](2); + + (before, action) = beforeAddLiquidity(action); + well.addLiquidity(amounts, lpAmountOut, user, type(uint256).max); + afterAddLiquidity(before, action); + + Snapshot memory beforeRemove; + RemoveLiquidityAction memory actionRemove; + actionRemove.lpAmountIn = well.getAddLiquidityOut(amounts); + actionRemove.amounts = amounts; + actionRemove.recipient = user; + + (beforeRemove, actionRemove) = beforeRemoveLiquidity(actionRemove); + well.removeLiquidity(lpAmountOut, amounts, user, type(uint256).max); + afterRemoveLiquidity(beforeRemove, actionRemove); + checkInvariant(address(well)); + } + + /// @dev Adding zero liquidity emits empty event but doesn't change reserves + function test_addLiquidity_zeroChange() public prank(user) { + uint256[] memory amounts = new uint256[](tokens.length); + uint256 liquidity = 0; + + Snapshot memory before; + AddLiquidityAction memory action; + action.amounts = amounts; + action.lpAmountOut = liquidity; + action.recipient = user; + action.fees = new uint256[](2); + + (before, action) = beforeAddLiquidity(action); + well.addLiquidity(amounts, liquidity, user, type(uint256).max); + afterAddLiquidity(before, action); + checkInvariant(address(well)); + } + + /// @dev Two-token fuzz test adding liquidity in any ratio + function testFuzz_addLiquidity(uint256 x, uint256 y) public prank(user) { + // amounts to add as liquidity + uint256[] memory amounts = new uint256[](2); + amounts[0] = bound(x, 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; + AddLiquidityAction memory action; + action.amounts = amounts; + action.lpAmountOut = well.getAddLiquidityOut(amounts); + action.recipient = user; + action.fees = new uint256[](2); + + (before, action) = beforeAddLiquidity(action); + well.addLiquidity(amounts, well.getAddLiquidityOut(amounts), user, type(uint256).max); + afterAddLiquidity(before, action); + checkInvariant(address(well)); + } +} diff --git a/test/Stable2/Well.Stable2.Bore.t.sol b/test/Stable2/Well.Stable2.Bore.t.sol new file mode 100644 index 00000000..662e4da3 --- /dev/null +++ b/test/Stable2/Well.Stable2.Bore.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {TestHelper, Well, IERC20, Call, Balances} from "test/TestHelper.sol"; +import {MockPump} from "mocks/pumps/MockPump.sol"; +import {Stable2} from "src/functions/Stable2.sol"; +import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; + +contract WellStable2BoreTest is TestHelper { + /// @dev Bore a 2-token Well with Stable2 & several pumps. + function setUp() public { + setupStable2Well(); + // Well.sol doesn't use wellData, so it should always return empty bytes + wellData = new bytes(0); + } + + //////////// Well Definition //////////// + + function test_tokens() public { + assertEq(well.tokens(), tokens); + } + + function test_wellFunction() public { + assertEq(well.wellFunction(), wellFunction); + } + + function test_pumps() public { + assertEq(well.pumps(), pumps); + } + + function test_wellData() public view { + assertEq(well.wellData(), wellData); + } + + function test_aquifer() public view { + assertEq(well.aquifer(), address(aquifer)); + } + + function test_well() public { + ( + IERC20[] memory _wellTokens, + Call memory _wellFunction, + Call[] memory _wellPumps, + bytes memory _wellData, + address _aquifer + ) = well.well(); + + assertEq(_wellTokens, tokens); + assertEq(_wellFunction, wellFunction); + assertEq(_wellPumps, pumps); + assertEq(_wellData, wellData); + assertEq(_aquifer, address(aquifer)); + } + + function test_getReserves() public view { + assertEq(well.getReserves(), getBalances(address(well), well).tokens); + } + + //////////// ERC20 LP Token //////////// + + function test_name() public view { + assertEq(well.name(), "TOKEN0:TOKEN1 Stable2 Well"); + } + + function test_symbol() public view { + assertEq(well.symbol(), "TOKEN0TOKEN1S2w"); + } + + function test_decimals() public view { + assertEq(well.decimals(), 18); + } + + //////////// Deployment //////////// + + /// @dev Fuzz different combinations of Well configuration data and check + /// that the Aquifer deploys everything correctly. + function testFuzz_bore(uint256 numberOfPumps, bytes[4] memory pumpData, uint256 nTokens, uint256 a) public { + // Constraints + numberOfPumps = bound(numberOfPumps, 0, 4); + for (uint256 i = 0; i < numberOfPumps; i++) { + vm.assume(pumpData[i].length <= 4 * 32); + } + nTokens = bound(nTokens, 2, tokens.length); + + vm.assume(a > 0); + // Get the first `nTokens` mock tokens + IERC20[] memory wellTokens = getTokens(nTokens); + bytes memory wellFunctionBytes = abi.encode(a, address(wellTokens[0]), address(wellTokens[1])); + + // Deploy a Well Function + address lut = address(new Stable2LUT1()); + wellFunction = Call(address(new Stable2(lut)), wellFunctionBytes); + + // Etch the MockPump at each `target` + Call[] memory pumps = new Call[](numberOfPumps); + for (uint256 i = 0; i < numberOfPumps; i++) { + pumps[i].target = address(new MockPump()); + pumps[i].data = pumpData[i]; + } + + // Deploy the Well + Well _well = + encodeAndBoreWell(address(aquifer), wellImplementation, wellTokens, wellFunction, pumps, bytes32(0)); + + // Check Pumps + assertEq(_well.numberOfPumps(), numberOfPumps, "number of pumps mismatch"); + Call[] memory _pumps = _well.pumps(); + + if (numberOfPumps > 0) { + assertEq(_well.firstPump(), pumps[0], "pump mismatch"); + } + + for (uint256 i = 0; i < numberOfPumps; i++) { + assertEq(_pumps[i], pumps[i], "pump mismatch"); + } + + // Check token addresses + assertEq(_well.tokens(), wellTokens); + + // Check Well Function + assertEq(_well.wellFunction(), wellFunction); + assertEq(_well.wellFunctionAddress(), wellFunction.target); + + // Check that Aquifer recorded the deployment + assertEq(aquifer.wellImplementation(address(_well)), wellImplementation); + } +} diff --git a/test/Stable2/Well.Stable2.RemoveLiquidity.t.sol b/test/Stable2/Well.Stable2.RemoveLiquidity.t.sol new file mode 100644 index 00000000..1e2dbfc0 --- /dev/null +++ b/test/Stable2/Well.Stable2.RemoveLiquidity.t.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {TestHelper, Stable2, IERC20, Balances} from "test/TestHelper.sol"; +import {Snapshot, AddLiquidityAction, RemoveLiquidityAction, LiquidityHelper} from "test/LiquidityHelper.sol"; +import {IWell} from "src/interfaces/IWell.sol"; +import {IWellErrors} from "src/interfaces/IWellErrors.sol"; +import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; + +contract WellStable2RemoveLiquidityTest is LiquidityHelper { + Stable2 ss; + bytes constant data = ""; + uint256 constant addedLiquidity = 1000 * 1e18; + + function setUp() public { + address lut = address(new Stable2LUT1()); + ss = new Stable2(lut); + setupStable2Well(); + + // Add liquidity. `user` now has (2 * 1000 * 1e27) LP tokens + addLiquidityEqualAmount(user, addedLiquidity); + } + + /// @dev ensure that Well liq was initialized correctly in {setUp} + /// currently, liquidity is added in {TestHelper} and above + function test_liquidityInitialized() public view { + IERC20[] memory tokens = well.tokens(); + for (uint256 i; i < tokens.length; i++) { + assertEq(tokens[i].balanceOf(address(well)), initialLiquidity + addedLiquidity, "incorrect token reserve"); + } + assertEq(well.totalSupply(), 4000 * 1e18, "incorrect totalSupply"); + } + + /// @dev getRemoveLiquidityOut: remove to equal amounts of underlying + /// since the tokens in the Well are balanced, user receives equal amounts + function test_getRemoveLiquidityOut() public view { + uint256[] memory amountsOut = well.getRemoveLiquidityOut(1000 * 1e18); + for (uint256 i; i < tokens.length; i++) { + assertEq(amountsOut[i], 500 * 1e18, "incorrect getRemoveLiquidityOut"); + } + } + + /// @dev removeLiquidity: reverts when user tries to remove too much of an underlying token + function test_removeLiquidity_revertIf_minAmountOutTooHigh() public prank(user) { + uint256 lpAmountIn = 1000 * 1e18; + + uint256[] memory minTokenAmountsOut = new uint256[](2); + minTokenAmountsOut[0] = 501 * 1e18; // too high + minTokenAmountsOut[1] = 500 * 1e18; + + vm.expectRevert(abi.encodeWithSelector(IWellErrors.SlippageOut.selector, 500 * 1e18, minTokenAmountsOut[0])); + well.removeLiquidity(lpAmountIn, minTokenAmountsOut, user, type(uint256).max); + } + + function test_removeLiquidity_revertIf_expired() public { + vm.expectRevert(IWellErrors.Expired.selector); + well.removeLiquidity(0, new uint256[](2), user, block.timestamp - 1); + } + + /// @dev removeLiquidity: remove to equal amounts of underlying + function test_removeLiquidity() public prank(user) { + uint256 lpAmountIn = 1000 * 1e18; + + uint256[] memory amountsOut = new uint256[](2); + amountsOut[0] = 500 * 1e18; + amountsOut[1] = 500 * 1e18; + + Snapshot memory before; + RemoveLiquidityAction memory action; + + action.amounts = amountsOut; + action.lpAmountIn = lpAmountIn; + action.recipient = user; + action.fees = new uint256[](2); + + (before, action) = beforeRemoveLiquidity(action); + well.removeLiquidity(lpAmountIn, amountsOut, user, type(uint256).max); + afterRemoveLiquidity(before, action); + checkInvariant(address(well)); + } + + /// @dev Fuzz test: EQUAL token reserves, BALANCED removal + /// The Well contains equal reserves of all underlying tokens before execution. + function test_removeLiquidity_fuzz(uint256 a0) public prank(user) { + // Setup amounts of liquidity to remove + // NOTE: amounts may or may not match the maximum removable by `user`. + uint256[] memory amounts = new uint256[](2); + amounts[0] = bound(a0, 0, 1000e18); + amounts[1] = amounts[0]; + + Snapshot memory before; + RemoveLiquidityAction memory action; + uint256 lpAmountIn = well.getRemoveLiquidityImbalancedIn(amounts); + + action.amounts = amounts; + action.lpAmountIn = lpAmountIn; + action.recipient = user; + action.fees = new uint256[](2); + + (before, action) = beforeRemoveLiquidity(action); + well.removeLiquidity(lpAmountIn, amounts, user, type(uint256).max); + afterRemoveLiquidity(before, action); + + assertLe( + well.totalSupply(), Stable2(wellFunction.target).calcLpTokenSupply(well.getReserves(), wellFunction.data) + ); + checkInvariant(address(well)); + } + + /// @dev Fuzz test: UNEQUAL token reserves, BALANCED removal + /// A Swap is performed by `user2` that imbalances the pool by `imbalanceBias` + /// before liquidity is removed by `user`. + function test_removeLiquidity_fuzzSwapBias(uint256 lpAmountBurned, uint256 imbalanceBias) public { + Balances memory userBalanceBeforeRemoveLiquidity = getBalances(user, well); + + uint256 maxLpAmountIn = userBalanceBeforeRemoveLiquidity.lp; + lpAmountBurned = bound(lpAmountBurned, 100, maxLpAmountIn); + imbalanceBias = bound(imbalanceBias, 0, 10e18); + + // `user2` performs a swap to imbalance the pool by `imbalanceBias` + vm.prank(user2); + well.swapFrom(tokens[0], tokens[1], imbalanceBias, 0, user2, type(uint256).max); + + // `user` has LP tokens and will perform a `removeLiquidity` call + vm.startPrank(user); + + uint256[] memory tokenAmountsOut = new uint256[](2); + tokenAmountsOut = well.getRemoveLiquidityOut(lpAmountBurned); + + Snapshot memory before; + RemoveLiquidityAction memory action; + + action.amounts = tokenAmountsOut; + action.lpAmountIn = lpAmountBurned; + action.recipient = user; + action.fees = new uint256[](2); + + (before, action) = beforeRemoveLiquidity(action); + well.removeLiquidity(lpAmountBurned, tokenAmountsOut, user, type(uint256).max); + afterRemoveLiquidity(before, action); + checkStableSwapInvariant(address(well)); + } +} diff --git a/test/Stable2/Well.Stable2.RemoveLiquidityImbalanced.t.sol b/test/Stable2/Well.Stable2.RemoveLiquidityImbalanced.t.sol new file mode 100644 index 00000000..2913cce8 --- /dev/null +++ b/test/Stable2/Well.Stable2.RemoveLiquidityImbalanced.t.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {TestHelper, Stable2, Balances} from "test/TestHelper.sol"; +import {IWell} from "src/interfaces/IWell.sol"; +import {IWellErrors} from "src/interfaces/IWellErrors.sol"; +import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; + +contract WellStable2RemoveLiquidityImbalancedTest is TestHelper { + event RemoveLiquidity(uint256 lpAmountIn, uint256[] tokenAmountsOut, address recipient); + + uint256[] tokenAmountsOut; + uint256 requiredLpAmountIn; + bytes _data; + + // Setup + Stable2 ss; + + uint256 constant addedLiquidity = 1000 * 1e18; + + function setUp() public { + address lut = address(new Stable2LUT1()); + ss = new Stable2(lut); + setupStable2Well(); + + _data = abi.encode(18, 18); + + // Add liquidity. `user` now has (2 * 1000 * 1e18) LP tokens + addLiquidityEqualAmount(user, addedLiquidity); + // Shared removal amounts + tokenAmountsOut.push(500 * 1e18); // 500 token0 + tokenAmountsOut.push(506 * 1e17); // 50.6 token1 + requiredLpAmountIn = 560_455_949_926_809_426_750; // ~552e18 LP needed to remove `tokenAmountsOut` + } + + /// @dev Assumes use of ConstantProduct2 + function test_getRemoveLiquidityImbalancedIn() public view { + uint256 lpAmountIn = well.getRemoveLiquidityImbalancedIn(tokenAmountsOut); + assertEq(lpAmountIn, requiredLpAmountIn); + } + + /// @dev not enough LP to receive `tokenAmountsOut` + function test_removeLiquidityImbalanced_revertIf_notEnoughLP() public prank(user) { + uint256 maxLpAmountIn = 5 * 1e18; + vm.expectRevert(abi.encodeWithSelector(IWellErrors.SlippageIn.selector, requiredLpAmountIn, maxLpAmountIn)); + well.removeLiquidityImbalanced(maxLpAmountIn, tokenAmountsOut, user, type(uint256).max); + } + + function test_removeLiquidityImbalanced_revertIf_expired() public { + vm.expectRevert(IWellErrors.Expired.selector); + well.removeLiquidityImbalanced(0, new uint256[](2), user, block.timestamp - 1); + } + + /// @dev Base case + function test_removeLiquidityImbalanced() public prank(user) { + Balances memory userBalanceBefore = getBalances(user, well); + + uint256 initialLpAmount = userBalanceBefore.lp; + uint256 maxLpAmountIn = requiredLpAmountIn; + + vm.expectEmit(true, true, true, true); + emit RemoveLiquidity(maxLpAmountIn, tokenAmountsOut, user); + well.removeLiquidityImbalanced(maxLpAmountIn, tokenAmountsOut, user, type(uint256).max); + + Balances memory userBalanceAfter = getBalances(user, well); + Balances memory wellBalanceAfter = getBalances(address(well), well); + + // `user` balance of LP tokens decreases + assertEq(userBalanceAfter.lp, initialLpAmount - maxLpAmountIn); + + // `user` balance of underlying tokens increases + // assumes initial balance of zero + assertEq(userBalanceAfter.tokens[0], tokenAmountsOut[0], "Incorrect token0 user balance"); + assertEq(userBalanceAfter.tokens[1], tokenAmountsOut[1], "Incorrect token1 user balance"); + + // Well's reserve of underlying tokens decreases + assertEq(wellBalanceAfter.tokens[0], 1500 * 1e18, "Incorrect token0 well reserve"); + assertEq(wellBalanceAfter.tokens[1], 19_494 * 1e17, "Incorrect token1 well reserve"); + checkInvariant(address(well)); + } + + /// @dev Fuzz test: EQUAL token reserves, IMBALANCED removal + /// The Well contains equal reserves of all underlying tokens before execution. + function testFuzz_removeLiquidityImbalanced(uint256 a0, uint256 a1) public prank(user) { + // Setup amounts of liquidity to remove + // NOTE: amounts may or may not be equal + uint256[] memory amounts = new uint256[](2); + amounts[0] = bound(a0, 0, 750e18); + amounts[1] = bound(a1, 0, 750e18); + + Balances memory wellBalanceBeforeRemoveLiquidity = getBalances(address(well), well); + Balances memory userBalanceBeforeRemoveLiquidity = getBalances(user, well); + // Calculate change in Well reserves after removing liquidity + uint256[] memory reserves = new uint256[](2); + reserves[0] = wellBalanceBeforeRemoveLiquidity.tokens[0] - amounts[0]; + reserves[1] = wellBalanceBeforeRemoveLiquidity.tokens[1] - amounts[1]; + + // lpAmountIn should be <= umaxLpAmountIn + uint256 maxLpAmountIn = userBalanceBeforeRemoveLiquidity.lp; + uint256 lpAmountIn = well.getRemoveLiquidityImbalancedIn(amounts); + + // Calculate the new LP token supply after the Well's reserves are changed. + // The delta `lpAmountBurned` is the amount of LP that should be burned + // when this liquidity is removed. + uint256 newLpTokenSupply = ss.calcLpTokenSupply(reserves, _data); + uint256 lpAmountBurned = well.totalSupply() - newLpTokenSupply; + + // Remove all of `user`'s liquidity and deliver them the tokens + vm.expectEmit(true, true, true, true); + emit RemoveLiquidity(lpAmountBurned, amounts, user); + well.removeLiquidityImbalanced(maxLpAmountIn, amounts, user, type(uint256).max); + + Balances memory userBalanceAfterRemoveLiquidity = getBalances(user, well); + Balances memory wellBalanceAfterRemoveLiquidity = getBalances(address(well), well); + + // `user` balance of LP tokens decreases + assertEq(userBalanceAfterRemoveLiquidity.lp, maxLpAmountIn - lpAmountIn, "Incorrect lp output"); + + // `user` balance of underlying tokens increases + assertEq(userBalanceAfterRemoveLiquidity.tokens[0], amounts[0], "Incorrect token0 user balance"); + assertEq(userBalanceAfterRemoveLiquidity.tokens[1], amounts[1], "Incorrect token1 user balance"); + + // Well's reserve of underlying tokens decreases + // Equal amount of liquidity of 1000e18 were added in the setup function hence the + // well's reserves here are 2000e18 minus the amounts removed, as the initial liquidity + // is 1000e18 of each token. + assertEq( + wellBalanceAfterRemoveLiquidity.tokens[0], + (initialLiquidity + addedLiquidity) - amounts[0], + "Incorrect token0 well reserve" + ); + assertEq( + wellBalanceAfterRemoveLiquidity.tokens[1], + (initialLiquidity + addedLiquidity) - amounts[1], + "Incorrect token1 well reserve" + ); + checkStableSwapInvariant(address(well)); + } + + /// @dev Fuzz test: UNEQUAL token reserves, IMBALANCED removal + /// A Swap is performed by `user2` that imbalances the pool by `imbalanceBias` + /// before liquidity is removed by `user`. + function testFuzz_removeLiquidityImbalanced_withSwap(uint256 a0, uint256 imbalanceBias) public { + // Setup amounts of liquidity to remove + // NOTE: amounts[0] is bounded at 1 to prevent slippage overflow + // failure, bug fix in progress + uint256[] memory amounts = new uint256[](2); + amounts[0] = bound(a0, 1, 950e18); + amounts[1] = amounts[0]; + imbalanceBias = bound(imbalanceBias, 0, 40e18); + + // `user2` performs a swap to imbalance the pool by `imbalanceBias` + vm.prank(user2); + well.swapFrom(tokens[0], tokens[1], imbalanceBias, 0, user2, type(uint256).max); + + // `user` has LP tokens and will perform a `removeLiquidityImbalanced` call + vm.startPrank(user); + + Balances memory wellBalanceBefore = getBalances(address(well), well); + Balances memory userBalanceBefore = getBalances(user, well); + + // Calculate change in Well reserves after removing liquidity + uint256[] memory reserves = new uint256[](2); + reserves[0] = wellBalanceBefore.tokens[0] - amounts[0]; + reserves[1] = wellBalanceBefore.tokens[1] - amounts[1]; + + // lpAmountIn should be <= user's LP balance + uint256 lpAmountIn = well.getRemoveLiquidityImbalancedIn(amounts); + + // Calculate the new LP token supply after the Well's reserves are changed. + // The delta `lpAmountBurned` is the amount of LP that should be burned + // when this liquidity is removed. + uint256 newLpTokenSupply = ss.calcLpTokenSupply(reserves, _data); + uint256 lpAmountBurned = well.totalSupply() - newLpTokenSupply; + + // Remove some of `user`'s liquidity and deliver them the tokens + uint256 maxLpAmountIn = userBalanceBefore.lp; + vm.expectEmit(true, true, true, true); + emit RemoveLiquidity(lpAmountBurned, amounts, user); + well.removeLiquidityImbalanced(maxLpAmountIn, amounts, user, type(uint256).max); + + Balances memory wellBalanceAfter = getBalances(address(well), well); + Balances memory userBalanceAfter = getBalances(user, well); + + // `user` balance of LP tokens decreases + assertEq(userBalanceAfter.lp, maxLpAmountIn - lpAmountIn, "Incorrect lp output"); + + // `user` balance of underlying tokens increases + assertEq(userBalanceAfter.tokens[0], userBalanceBefore.tokens[0] + amounts[0], "Incorrect token0 user balance"); + assertEq(userBalanceAfter.tokens[1], userBalanceBefore.tokens[1] + amounts[1], "Incorrect token1 user balance"); + + // Well's reserve of underlying tokens decreases + assertEq(wellBalanceAfter.tokens[0], wellBalanceBefore.tokens[0] - amounts[0], "Incorrect token0 well reserve"); + assertEq(wellBalanceAfter.tokens[1], wellBalanceBefore.tokens[1] - amounts[1], "Incorrect token1 well reserve"); + checkInvariant(address(well)); + } +} diff --git a/test/Stable2/Well.Stable2.RemoveLiquidityOneToken.t.sol b/test/Stable2/Well.Stable2.RemoveLiquidityOneToken.t.sol new file mode 100644 index 00000000..6ca26382 --- /dev/null +++ b/test/Stable2/Well.Stable2.RemoveLiquidityOneToken.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {TestHelper, Stable2, IERC20, Balances} from "test/TestHelper.sol"; +import {IWell} from "src/interfaces/IWell.sol"; +import {IWellErrors} from "src/interfaces/IWellErrors.sol"; +import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; + +contract WellStable2RemoveLiquidityOneTokenTest is TestHelper { + event RemoveLiquidityOneToken(uint256 lpAmountIn, IERC20 tokenOut, uint256 tokenAmountOut, address recipient); + + Stable2 ss; + uint256 constant addedLiquidity = 1000 * 1e18; + bytes _data; + + function setUp() public { + address lut = address(new Stable2LUT1()); + ss = new Stable2(lut); + setupStable2Well(); + + // Add liquidity. `user` now has (2 * 1000 * 1e18) LP tokens + addLiquidityEqualAmount(user, addedLiquidity); + _data = abi.encode(18, 18); + } + + /// @dev Assumes use of Stable2 + function test_getRemoveLiquidityOneTokenOut() public view { + uint256 amountOut = well.getRemoveLiquidityOneTokenOut(500 * 1e18, tokens[0]); + assertEq(amountOut, 488_542_119_171_820_114_601, "incorrect tokenOut"); + } + + /// @dev not enough tokens received for `lpAmountIn`. + function test_removeLiquidityOneToken_revertIf_amountOutTooLow() public prank(user) { + uint256 lpAmountIn = 500 * 1e18; + uint256 minTokenAmountOut = 500 * 1e18; + uint256 amountOut = well.getRemoveLiquidityOneTokenOut(lpAmountIn, tokens[0]); + + vm.expectRevert(abi.encodeWithSelector(IWellErrors.SlippageOut.selector, amountOut, minTokenAmountOut)); + well.removeLiquidityOneToken(lpAmountIn, tokens[0], minTokenAmountOut, user, type(uint256).max); + } + + function test_removeLiquidityOneToken_revertIf_expired() public { + vm.expectRevert(IWellErrors.Expired.selector); + well.removeLiquidityOneToken(0, tokens[0], 0, user, block.timestamp - 1); + } + + /// @dev Base case + function test_removeLiquidityOneToken() public prank(user) { + uint256 lpAmountIn = 500 * 1e18; + uint256 minTokenAmountOut = 488_542_119_171_820_114_601; + Balances memory prevUserBalance = getBalances(user, well); + + vm.expectEmit(true, true, true, true); + emit RemoveLiquidityOneToken(lpAmountIn, tokens[0], minTokenAmountOut, user); + + uint256 amountOut = + well.removeLiquidityOneToken(lpAmountIn, tokens[0], minTokenAmountOut, user, type(uint256).max); + + Balances memory userBalance = getBalances(user, well); + Balances memory wellBalance = getBalances(address(well), well); + + assertEq(userBalance.lp, prevUserBalance.lp - lpAmountIn, "Incorrect lpAmountIn"); + + assertEq(userBalance.tokens[0], amountOut, "Incorrect token0 user balance"); + assertEq(userBalance.tokens[1], 0, "Incorrect token1 user balance"); + + // Equal amount of liquidity of 1000e18 were added in the setup function hence the + // well's reserves here are 2000e18 minus the amounts removed, as the initial liquidity + // is 1000e18 of each token. + assertEq( + wellBalance.tokens[0], + (initialLiquidity + addedLiquidity) - minTokenAmountOut, + "Incorrect token0 well reserve" + ); + assertEq(wellBalance.tokens[1], (initialLiquidity + addedLiquidity), "Incorrect token1 well reserve"); + checkStableSwapInvariant(address(well)); + } + + /// @dev Fuzz test: EQUAL token reserves, IMBALANCED removal + /// The Well contains equal reserves of all underlying tokens before execution. + function testFuzz_removeLiquidityOneToken(uint256 a0) public prank(user) { + // Assume we're removing tokens[0] + uint256[] memory amounts = new uint256[](2); + amounts[0] = bound(a0, 1e18, 750e18); + amounts[1] = 0; + + Balances memory userBalanceBeforeRemoveLiquidity = getBalances(user, well); + uint256 userLpBalance = userBalanceBeforeRemoveLiquidity.lp; + + // Find the LP amount that should be burned given the fuzzed + // amounts. Works even though only amounts[0] is set. + uint256 lpAmountIn = well.getRemoveLiquidityImbalancedIn(amounts); + + Balances memory wellBalanceBeforeRemoveLiquidity = getBalances(address(well), well); + + // Calculate change in Well reserves after removing liquidity + uint256[] memory reserves = new uint256[](2); + reserves[0] = wellBalanceBeforeRemoveLiquidity.tokens[0] - amounts[0]; + reserves[1] = wellBalanceBeforeRemoveLiquidity.tokens[1] - amounts[1]; // should stay the same + + // Calculate the new LP token supply after the Well's reserves are changed. + // The delta `lpAmountBurned` is the amount of LP that should be burned + // when this liquidity is removed. + uint256 newLpTokenSupply = ss.calcLpTokenSupply(reserves, _data); + uint256 lpAmountBurned = well.totalSupply() - newLpTokenSupply; + vm.expectEmit(true, true, true, false); + emit RemoveLiquidityOneToken(lpAmountBurned, tokens[0], amounts[0], user); + uint256 amountOut = well.removeLiquidityOneToken(lpAmountIn, tokens[0], 0, user, type(uint256).max); // no minimum out + assertApproxEqAbs(amountOut, amounts[0], 2, "amounts[0] > userLpBalance"); + + Balances memory userBalanceAfterRemoveLiquidity = getBalances(user, well); + Balances memory wellBalanceAfterRemoveLiquidity = getBalances(address(well), well); + + assertEq(userBalanceAfterRemoveLiquidity.lp, userLpBalance - lpAmountIn, "Incorrect lp output"); + assertApproxEqAbs(userBalanceAfterRemoveLiquidity.tokens[0], amounts[0], 2, "Incorrect token0 user balance"); + assertApproxEqAbs(userBalanceAfterRemoveLiquidity.tokens[1], amounts[1], 2, "Incorrect token1 user balance"); // should stay the same + assertApproxEqAbs( + wellBalanceAfterRemoveLiquidity.tokens[0], + (initialLiquidity + addedLiquidity) - amounts[0], + 2, + "Incorrect token0 well reserve" + ); + assertEq( + wellBalanceAfterRemoveLiquidity.tokens[1], + (initialLiquidity + addedLiquidity) - amounts[1], + "Incorrect token1 well reserve" + ); // should stay the same + checkStableSwapInvariant(address(well)); + } + + // TODO: fuzz test: imbalanced ratio of tokens +} diff --git a/test/Stable2/Well.Stable2.Shift.t.sol b/test/Stable2/Well.Stable2.Shift.t.sol new file mode 100644 index 00000000..e2873e84 --- /dev/null +++ b/test/Stable2/Well.Stable2.Shift.t.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {TestHelper, Balances, ConstantProduct2, IERC20, Stable2} from "test/TestHelper.sol"; +import {IWell} from "src/interfaces/IWell.sol"; +import {IWellErrors} from "src/interfaces/IWellErrors.sol"; + +contract WellStable2ShiftTest is TestHelper { + event Shift(uint256[] reserves, IERC20 toToken, uint256 minAmountOut, address recipient); + + function setUp() public { + setupStable2Well(); + } + + /// @dev Shift excess token0 into token1. + function testFuzz_shift(uint256 amount) public prank(user) { + amount = bound(amount, 1, 1000e18); + + // Transfer `amount` of token0 to the Well + tokens[0].transfer(address(well), amount); + Balances memory wellBalanceBeforeShift = getBalances(address(well), well); + assertEq(wellBalanceBeforeShift.tokens[0], 1000e18 + amount, "Well should have received token0"); + assertEq(wellBalanceBeforeShift.tokens[1], 1000e18, "Well should have NOT have received token1"); + + // Get a user with a fresh address (no ERC20 tokens) + address _user = users.getNextUserAddress(); + Balances memory userBalanceBeforeShift = getBalances(_user, well); + + // Verify that `_user` has no tokens + assertEq(userBalanceBeforeShift.tokens[0], 0, "User should start with 0 of token0"); + assertEq(userBalanceBeforeShift.tokens[1], 0, "User should start with 0 of token1"); + + uint256 minAmountOut = well.getShiftOut(tokens[1]); + uint256[] memory calcReservesAfter = new uint256[](2); + calcReservesAfter[0] = tokens[0].balanceOf(address(well)); + calcReservesAfter[1] = tokens[1].balanceOf(address(well)) - minAmountOut; + + vm.expectEmit(true, true, true, true); + emit Shift(calcReservesAfter, tokens[1], minAmountOut, _user); + uint256 amtOut = well.shift(tokens[1], minAmountOut, _user); + + uint256[] memory reserves = well.getReserves(); + Balances memory userBalanceAfterShift = getBalances(_user, well); + Balances memory wellBalanceAfterShift = getBalances(address(well), well); + + // User should have gained token1 + assertEq(userBalanceAfterShift.tokens[0], 0, "User should NOT have gained token0"); + assertEq(userBalanceAfterShift.tokens[1], amtOut, "User should have gained token1"); + assertTrue(userBalanceAfterShift.tokens[1] >= userBalanceBeforeShift.tokens[1], "User should have more token1"); + + // Reserves should now match balances + assertEq(wellBalanceAfterShift.tokens[0], reserves[0], "Well should have correct token0 balance"); + assertEq(wellBalanceAfterShift.tokens[1], reserves[1], "Well should have correct token1 balance"); + + // The difference has been sent to _user. + assertEq( + userBalanceAfterShift.tokens[1], + wellBalanceBeforeShift.tokens[1] - wellBalanceAfterShift.tokens[1], + "User should have correct token1 balance" + ); + assertEq( + userBalanceAfterShift.tokens[1], + userBalanceBeforeShift.tokens[1] + amtOut, + "User should have correct token1 balance" + ); + checkStableSwapInvariant(address(well)); + } + + /// @dev Shift excess token0 into token0 (just transfers the excess token0 to the user). + function testFuzz_shift_tokenOut(uint256 amount) public prank(user) { + amount = bound(amount, 1, 1000e18); + + // Transfer `amount` of token0 to the Well + tokens[0].transfer(address(well), amount); + Balances memory wellBalanceBeforeShift = getBalances(address(well), well); + assertEq(wellBalanceBeforeShift.tokens[0], 1000e18 + amount, "Well should have received tokens"); + + // Get a user with a fresh address (no ERC20 tokens) + address _user = users.getNextUserAddress(); + Balances memory userBalanceBeforeShift = getBalances(_user, well); + + // Verify that the user has no tokens + assertEq(userBalanceBeforeShift.tokens[0], 0, "User should start with 0 of token0"); + assertEq(userBalanceBeforeShift.tokens[1], 0, "User should start with 0 of token1"); + + uint256 minAmountOut = well.getShiftOut(tokens[0]); + uint256[] memory calcReservesAfter = new uint256[](2); + calcReservesAfter[0] = tokens[0].balanceOf(address(well)) - minAmountOut; + calcReservesAfter[1] = tokens[1].balanceOf(address(well)); + + vm.expectEmit(true, true, true, true); + emit Shift(calcReservesAfter, tokens[0], minAmountOut, _user); + // Shift the imbalanced token as the token out + well.shift(tokens[0], 0, _user); + + uint256[] memory reserves = well.getReserves(); + Balances memory userBalanceAfterShift = getBalances(_user, well); + Balances memory wellBalanceAfterShift = getBalances(address(well), well); + + // User should have gained token0 + assertEq(userBalanceAfterShift.tokens[0], amount, "User should have gained token0"); + assertEq( + userBalanceAfterShift.tokens[1], userBalanceBeforeShift.tokens[1], "User should NOT have gained token1" + ); + + // Reserves should now match balances + assertEq(wellBalanceAfterShift.tokens[0], reserves[0], "Well should have correct token0 balance"); + assertEq(wellBalanceAfterShift.tokens[1], reserves[1], "Well should have correct token1 balance"); + + assertEq( + userBalanceAfterShift.tokens[0], + userBalanceBeforeShift.tokens[0] + amount, + "User should have gained token 1" + ); + checkInvariant(address(well)); + } + + /// @dev Calling shift() on a balanced Well should do nothing. + function test_shift_balanced_pool() public prank(user) { + Balances memory wellBalanceBeforeShift = getBalances(address(well), well); + assertEq(wellBalanceBeforeShift.tokens[0], wellBalanceBeforeShift.tokens[1], "Well should should be balanced"); + + // Get a user with a fresh address (no ERC20 tokens) + address _user = users.getNextUserAddress(); + Balances memory userBalanceBeforeShift = getBalances(_user, well); + + // Verify that the user has no tokens + assertEq(userBalanceBeforeShift.tokens[0], 0, "User should start with 0 of token0"); + assertEq(userBalanceBeforeShift.tokens[1], 0, "User should start with 0 of token1"); + + well.shift(tokens[1], 0, _user); + + uint256[] memory reserves = well.getReserves(); + Balances memory userBalanceAfterShift = getBalances(_user, well); + Balances memory wellBalanceAfterShift = getBalances(address(well), well); + + // User should have gained neither token + assertEq(userBalanceAfterShift.tokens[0], 0, "User should NOT have gained token0"); + assertEq(userBalanceAfterShift.tokens[1], 0, "User should NOT have gained token1"); + + // Reserves should equal balances + assertEq(wellBalanceAfterShift.tokens[0], reserves[0], "Well should have correct token0 balance"); + assertEq(wellBalanceAfterShift.tokens[1], reserves[1], "Well should have correct token1 balance"); + checkInvariant(address(well)); + } + + function test_shift_fail_slippage(uint256 amount) public prank(user) { + amount = bound(amount, 1, 1000e18); + + // Transfer `amount` of token0 to the Well + tokens[0].transfer(address(well), amount); + Balances memory wellBalanceBeforeShift = getBalances(address(well), well); + assertEq(wellBalanceBeforeShift.tokens[0], 1000e18 + amount, "Well should have received token0"); + assertEq(wellBalanceBeforeShift.tokens[1], 1000e18, "Well should have NOT have received token1"); + + uint256 amountOut = well.getShiftOut(tokens[1]); + vm.expectRevert(abi.encodeWithSelector(IWellErrors.SlippageOut.selector, amountOut, type(uint256).max)); + well.shift(tokens[1], type(uint256).max, user); + } +} diff --git a/test/Stable2/Well.Stable2.Skim.t.sol b/test/Stable2/Well.Stable2.Skim.t.sol new file mode 100644 index 00000000..190f1cc0 --- /dev/null +++ b/test/Stable2/Well.Stable2.Skim.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {TestHelper, Balances} from "test/TestHelper.sol"; + +contract WellStable2SkimTest is TestHelper { + function setUp() public { + setupStable2Well(); + } + + function test_initialized() public { + // Well should have liquidity + Balances memory wellBalance = getBalances(address(well), well); + assertEq(wellBalance.tokens[0], 1000e18); + assertEq(wellBalance.tokens[1], 1000e18); + } + + function testFuzz_skim(uint256[2] calldata amounts) public prank(user) { + vm.assume(amounts[0] <= 800e18); + vm.assume(amounts[1] <= 800e18); + + // Transfer from Test contract to Well + tokens[0].transfer(address(well), amounts[0]); + tokens[1].transfer(address(well), amounts[1]); + + Balances memory wellBalanceBeforeSkim = getBalances(address(well), well); + // Verify that the Well has received the tokens + assertEq(wellBalanceBeforeSkim.tokens[0], 1000e18 + amounts[0]); + assertEq(wellBalanceBeforeSkim.tokens[1], 1000e18 + amounts[1]); + + // Get a user with a fresh address (no ERC20 tokens) + address _user = users.getNextUserAddress(); + uint256[] memory reserves = new uint256[](2); + + // Verify that the user has no tokens + Balances memory userBalanceBeforeSkim = getBalances(_user, well); + reserves[0] = userBalanceBeforeSkim.tokens[0]; + reserves[1] = userBalanceBeforeSkim.tokens[1]; + assertEq(reserves[0], 0); + assertEq(reserves[1], 0); + + well.skim(_user); + + Balances memory userBalanceAfterSkim = getBalances(_user, well); + Balances memory wellBalanceAfterSkim = getBalances(address(well), well); + + // Since only 1000e18 of each token was added as liquidity, the Well's reserve + // should be reset back to this. + assertEq(wellBalanceAfterSkim.tokens[0], 1000e18); + assertEq(wellBalanceAfterSkim.tokens[1], 1000e18); + + // The difference has been sent to _user. + assertEq(userBalanceAfterSkim.tokens[0], amounts[0]); + assertEq(userBalanceAfterSkim.tokens[1], amounts[1]); + } +} diff --git a/test/Stable2/Well.Stable2.SwapFrom.t.sol b/test/Stable2/Well.Stable2.SwapFrom.t.sol new file mode 100644 index 00000000..436e421a --- /dev/null +++ b/test/Stable2/Well.Stable2.SwapFrom.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IERC20, Balances, Call, MockToken, Well, console} from "test/TestHelper.sol"; +import {SwapHelper, SwapAction, Snapshot} from "test/SwapHelper.sol"; +import {MockFunctionBad} from "mocks/functions/MockFunctionBad.sol"; +import {IWellFunction} from "src/interfaces/IWellFunction.sol"; +import {IWell} from "src/interfaces/IWell.sol"; +import {IWellErrors} from "src/interfaces/IWellErrors.sol"; + +contract WellStable2SwapFromTest is SwapHelper { + function setUp() public { + setupStable2Well(); + } + + function test_getSwapOut() public view { + uint256 amountIn = 10 * 1e18; + uint256 amountOut = well.getSwapOut(tokens[0], tokens[1], amountIn); + + assertEq(amountOut, 9_966_775_941_840_933_593); + } + + function testFuzz_getSwapOut_revertIf_insufficientWellBalance(uint256 amountIn, uint256 i) public prank(user) { + // Swap token `i` -> all other tokens + vm.assume(i < tokens.length); + + // Find an input amount that produces an output amount higher than what the Well has. + // When the Well is deployed it has zero reserves, so any nonzero value should revert. + amountIn = bound(amountIn, 1, type(uint128).max); + + // Deploy a new Well with a poorly engineered pricing function. + // Its `getBalance` function can return an amount greater than the Well holds. + IWellFunction badFunction = new MockFunctionBad(); + Well badWell = encodeAndBoreWell( + address(aquifer), wellImplementation, tokens, Call(address(badFunction), ""), pumps, bytes32(0) + ); + + // Check assumption that reserves are empty + Balances memory wellBalances = getBalances(address(badWell), badWell); + assertEq(wellBalances.tokens[0], 0, "bad assumption: wellBalances.tokens[0] != 0"); + assertEq(wellBalances.tokens[1], 0, "bad assumption: wellBalances.tokens[1] != 0"); + + for (uint256 j = 0; j < tokens.length; ++j) { + if (j != i) { + vm.expectRevert(); // underflow + badWell.getSwapOut(tokens[i], tokens[j], amountIn); + } + } + } + + /// @dev Swaps should always revert if `fromToken` = `toToken`. + function test_swapFrom_revertIf_sameToken() public prank(user) { + vm.expectRevert(IWellErrors.InvalidTokens.selector); + well.swapFrom(tokens[0], tokens[0], 100 * 1e18, 0, user, type(uint256).max); + } + + /// @dev Slippage revert if minAmountOut is too high + function test_swapFrom_revertIf_minAmountOutTooHigh() public prank(user) { + uint256 amountIn = 10 * 1e18; + uint256 amountOut = well.getSwapOut(tokens[0], tokens[1], amountIn); + uint256 minAmountOut = amountOut + 1e18; + + vm.expectRevert(abi.encodeWithSelector(IWellErrors.SlippageOut.selector, amountOut, minAmountOut)); + well.swapFrom(tokens[0], tokens[1], amountIn, minAmountOut, user, type(uint256).max); + } + + function test_swapFrom_revertIf_expired() public { + vm.expectRevert(IWellErrors.Expired.selector); + well.swapFrom(tokens[0], tokens[1], 0, 0, user, block.timestamp - 1); + } + + function testFuzz_swapFrom(uint256 amountIn) public prank(user) { + amountIn = bound(amountIn, 0, tokens[0].balanceOf(user)); + + (Snapshot memory bef, SwapAction memory act) = beforeSwapFrom(0, 1, amountIn); + act.wellSends = well.swapFrom(tokens[0], tokens[1], amountIn, 0, user, type(uint256).max); + afterSwapFrom(bef, act); + checkStableSwapInvariant(address(well)); + } + + function testFuzz_swapAndRemoveAllLiq(uint256 amountIn) public { + amountIn = bound(amountIn, 0, tokens[0].balanceOf(user)); + vm.prank(user); + well.swapFrom(tokens[0], tokens[1], amountIn, 0, user, type(uint256).max); + + vm.prank(address(this)); + well.removeLiquidityImbalanced( + type(uint256).max, IWell(address(well)).getReserves(), address(this), type(uint256).max + ); + assertEq(IERC20(address(well)).totalSupply(), 0); + } + + /// @dev Zero hysteresis: token0 -> token1 -> token0 gives the same result + function testFuzz_swapFrom_equalSwap(uint256 token0AmtIn) public prank(user) { + vm.assume(token0AmtIn < tokens[0].balanceOf(user)); + uint256 token1Out = well.swapFrom(tokens[0], tokens[1], token0AmtIn, 0, user, type(uint256).max); + uint256 token0Out = well.swapFrom(tokens[1], tokens[0], token1Out, 0, user, type(uint256).max); + assertEq(token0Out, token0AmtIn); + checkInvariant(address(well)); + } +} diff --git a/test/Stable2/Well.Stable2.SwapTo.t.sol b/test/Stable2/Well.Stable2.SwapTo.t.sol new file mode 100644 index 00000000..067b5d02 --- /dev/null +++ b/test/Stable2/Well.Stable2.SwapTo.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IERC20, Balances, Call, MockToken, Well} from "test/TestHelper.sol"; +import {SwapHelper, SwapAction} from "test/SwapHelper.sol"; +import {MockFunctionBad} from "mocks/functions/MockFunctionBad.sol"; +import {IWellFunction} from "src/interfaces/IWellFunction.sol"; +import {IWell} from "src/interfaces/IWell.sol"; +import {IWellErrors} from "src/interfaces/IWellErrors.sol"; + +contract WellStable2SwapToTest is SwapHelper { + function setUp() public { + // init 1000e18, 1000e18 + setupStable2Well(); + } + + function test_getSwapIn() public view { + uint256 amountOut = 100 * 1e18; + uint256 amountIn = well.getSwapIn(tokens[0], tokens[1], amountOut); + assertEq(amountIn, 103_464_719_546_263_310_322); // ~3% slippage + } + + function testFuzz_getSwapIn_revertIf_insufficientWellBalance(uint256 i) public prank(user) { + IERC20[] memory _tokens = well.tokens(); + Balances memory wellBalances = getBalances(address(well), well); + vm.assume(i < _tokens.length); + + // Swap token `i` -> all other tokens + for (uint256 j; j < _tokens.length; ++j) { + if (j != i) { + // Request to buy more of {_tokens[j]} than the Well has. + // There is no input amount that could complete this Swap. + uint256 amountOut = wellBalances.tokens[j] + 1; + vm.expectRevert(); // underflow + well.getSwapIn(_tokens[i], _tokens[j], amountOut); + } + } + } + + /// @dev Swaps should always revert if `fromToken` = `toToken`. + function test_swapTo_revertIf_sameToken() public prank(user) { + vm.expectRevert(IWellErrors.InvalidTokens.selector); + well.swapTo(tokens[0], tokens[0], 100 * 1e18, 0, user, type(uint256).max); + } + + /// @dev Slippage revert occurs if maxAmountIn is too low + function test_swapTo_revertIf_maxAmountInTooLow() public prank(user) { + uint256 amountOut = 100 * 1e18; + uint256 amountIn = 103_464_719_546_263_310_322; + uint256 maxAmountIn = (amountIn * 99) / 100; + + vm.expectRevert(abi.encodeWithSelector(IWellErrors.SlippageIn.selector, amountIn, maxAmountIn)); + well.swapTo(tokens[0], tokens[1], maxAmountIn, amountOut, user, type(uint256).max); + } + + /// @dev Note: this covers the case where there is a fee as well + function test_swapFromFeeOnTransferNoFee_revertIf_expired() public { + vm.expectRevert(IWellErrors.Expired.selector); + well.swapTo(tokens[0], tokens[1], 0, 0, user, block.timestamp - 1); + } + + /// @dev tests assume 2 tokens in future we can extend for multiple tokens + function testFuzz_swapTo(uint256 amountOut) public prank(user) { + // User has 1000 of each token + // Given current liquidity, swapping 1000 of one token gives 500 of the other + uint256 maxAmountIn = 1000 * 1e18; + amountOut = bound(amountOut, 0, 500 * 1e18); + + Balances memory userBalancesBefore = getBalances(user, well); + Balances memory wellBalancesBefore = getBalances(address(well), well); + + // Decrease reserve of token 1 by `amountOut` which is paid to user + uint256[] memory calcBalances = new uint256[](wellBalancesBefore.tokens.length); + calcBalances[0] = wellBalancesBefore.tokens[0]; + calcBalances[1] = wellBalancesBefore.tokens[1] - amountOut; + + uint256 calcAmountIn = IWellFunction(wellFunction.target).calcReserve( + calcBalances, + 0, // j + wellBalancesBefore.lpSupply, + wellFunction.data + ) - wellBalancesBefore.tokens[0]; + + vm.expectEmit(true, true, true, true); + emit Swap(tokens[0], tokens[1], calcAmountIn, amountOut, user); + well.swapTo(tokens[0], tokens[1], maxAmountIn, amountOut, user, type(uint256).max); + + Balances memory userBalancesAfter = getBalances(user, well); + Balances memory wellBalancesAfter = getBalances(address(well), well); + + assertEq( + userBalancesBefore.tokens[0] - userBalancesAfter.tokens[0], calcAmountIn, "Incorrect token0 user balance" + ); + assertEq(userBalancesAfter.tokens[1] - userBalancesBefore.tokens[1], amountOut, "Incorrect token1 user balance"); + assertEq( + wellBalancesAfter.tokens[0], wellBalancesBefore.tokens[0] + calcAmountIn, "Incorrect token0 well reserve" + ); + assertEq(wellBalancesAfter.tokens[1], wellBalancesBefore.tokens[1] - amountOut, "Incorrect token1 well reserve"); + checkStableSwapInvariant(address(well)); + } + + /// @dev Zero hysteresis: token0 -> token1 -> token0 gives the same result + function testFuzz_swapTo_equalSwap(uint256 token0AmtOut) public prank(user) { + // assume amtOut is lower due to slippage + vm.assume(token0AmtOut < 500e18); + uint256 token1In = well.swapTo(tokens[0], tokens[1], 1000e18, token0AmtOut, user, type(uint256).max); + uint256 token0In = well.swapTo(tokens[1], tokens[0], 1000e18, token1In, user, type(uint256).max); + assertEq(token0In, token0AmtOut); + checkInvariant(address(well)); + } +} diff --git a/test/TestHelper.sol b/test/TestHelper.sol index f8fef634..3289be13 100644 --- a/test/TestHelper.sol +++ b/test/TestHelper.sol @@ -13,6 +13,8 @@ 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 {Stable2} from "src/functions/Stable2.sol"; +import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; import {WellDeployer} from "script/helpers/WellDeployer.sol"; @@ -126,6 +128,45 @@ abstract contract TestHelper is Test, WellDeployer { user2 = _user[1]; } + function setupStable2Well() internal { + setupStable2Well(deployPumps(1), deployMockTokens(2)); + } + + function setupStable2Well(Call[] memory _pumps, IERC20[] memory _tokens) internal { + // deploy new LUT: + address lut = address(new Stable2LUT1()); + // encode wellFunction Data + bytes memory wellFunctionData = + abi.encode(MockToken(address(_tokens[0])).decimals(), MockToken(address(_tokens[1])).decimals()); + Call memory _wellFunction = Call(address(new Stable2(lut)), wellFunctionData); + tokens = _tokens; + wellFunction = _wellFunction; + vm.label(address(wellFunction.target), "Stable2 WF"); + for (uint256 i = 0; i < _pumps.length; i++) { + pumps.push(_pumps[i]); + } + + initUser(); + + wellImplementation = deployWellImplementation(); + aquifer = new Aquifer(); + well = encodeAndBoreWell(address(aquifer), wellImplementation, tokens, _wellFunction, _pumps, bytes32(0)); + vm.label(address(well), "Stable2Well"); + + // Mint mock tokens to user + mintTokens(user, initialLiquidity); + mintTokens(user2, initialLiquidity); + approveMaxTokens(user, address(well)); + approveMaxTokens(user2, address(well)); + + // Mint mock tokens to TestHelper + mintTokens(address(this), initialLiquidity); + approveMaxTokens(address(this), address(well)); + + // Add initial liquidity from TestHelper + addLiquidityEqualAmount(address(this), initialLiquidity); + } + //////////// Test Tokens //////////// /// @dev deploy `n` mock ERC20 tokens and sort by address @@ -146,6 +187,16 @@ abstract contract TestHelper is Test, WellDeployer { ); } + function deployMockTokenWithDecimals(uint256 i, uint8 decimals) internal returns (IERC20) { + return IERC20( + new MockToken( + string.concat("Token ", i.toString()), // name + string.concat("TOKEN", i.toString()), // symbol + decimals // decimals + ) + ); + } + /// @dev deploy `n` mock ERC20 tokens and sort by address function deployMockTokensFeeOnTransfer(uint256 n) internal returns (IERC20[] memory _tokens) { _tokens = new IERC20[](n); @@ -200,6 +251,19 @@ abstract contract TestHelper is Test, WellDeployer { _wellFunction.data = new bytes(0); } + function deployWellFunction(address _target) internal pure returns (Call memory _wellFunction) { + _wellFunction.target = _target; + _wellFunction.data = new bytes(0); + } + + function deployWellFunction( + address _target, + bytes memory _data + ) internal pure returns (Call memory _wellFunction) { + _wellFunction.target = _target; + _wellFunction.data = _data; + } + function deployPumps(uint256 n) internal returns (Call[] memory _pumps) { _pumps = new Call[](n); for (uint256 i; i < n; i++) { @@ -255,39 +319,39 @@ abstract contract TestHelper is Test, WellDeployer { //////////// Assertions //////////// - function assertEq(IERC20 a, IERC20 b) internal { + function assertEq(IERC20 a, IERC20 b) internal pure { assertEq(a, b, "Address mismatch"); } - function assertEq(IERC20 a, IERC20 b, string memory err) internal { + function assertEq(IERC20 a, IERC20 b, string memory err) internal pure { assertEq(address(a), address(b), err); } - function assertEq(IERC20[] memory a, IERC20[] memory b) internal { + function assertEq(IERC20[] memory a, IERC20[] memory b) internal pure { assertEq(a, b, "IERC20[] mismatch"); } - function assertEq(IERC20[] memory a, IERC20[] memory b, string memory err) internal { + function assertEq(IERC20[] memory a, IERC20[] memory b, string memory err) internal pure { assertEq(a.length, b.length, err); for (uint256 i; i < a.length; i++) { assertEq(a[i], b[i], err); // uses the prev overload } } - function assertEq(Call memory a, Call memory b) internal { + function assertEq(Call memory a, Call memory b) internal pure { assertEq(a, b, "Call mismatch"); } - function assertEq(Call memory a, Call memory b, string memory err) internal { + function assertEq(Call memory a, Call memory b, string memory err) internal pure { assertEq(a.target, b.target, err); assertEq(a.data, b.data, err); } - function assertEq(Call[] memory a, Call[] memory b) internal { + function assertEq(Call[] memory a, Call[] memory b) internal pure { assertEq(a, b, "Call[] mismatch"); } - function assertEq(Call[] memory a, Call[] memory b, string memory err) internal { + function assertEq(Call[] memory a, Call[] memory b, string memory err) internal pure { assertEq(a.length, b.length, err); for (uint256 i; i < a.length; i++) { assertEq(a[i], b[i], err); // uses the prev overload @@ -298,7 +362,7 @@ abstract contract TestHelper is Test, WellDeployer { assertApproxEqRelN(a, b, 1, precision); } - function assertApproxLeRelN(uint256 a, uint256 b, uint256 precision, uint256 absoluteError) internal { + function assertApproxLeRelN(uint256 a, uint256 b, uint256 precision, uint256 absoluteError) internal pure { console.log("A: %s", a); console.log("B: %s", b); console.log(precision); @@ -319,7 +383,7 @@ abstract contract TestHelper is Test, WellDeployer { } } - function assertApproxGeRelN(uint256 a, uint256 b, uint256 precision, uint256 absoluteError) internal { + function assertApproxGeRelN(uint256 a, uint256 b, uint256 precision, uint256 absoluteError) internal pure { console.log("A: %s", a); console.log("B: %s", b); console.log(precision); @@ -363,7 +427,7 @@ abstract contract TestHelper is Test, WellDeployer { function percentDeltaN(uint256 a, uint256 b, uint256 precision) internal pure returns (uint256) { uint256 absDelta = stdMath.delta(a, b); - return absDelta * (10 ** precision) / b; + return (absDelta * (10 ** precision)) / b; } function _newSnapshot() internal view returns (Snapshot memory snapshot) { @@ -372,12 +436,23 @@ abstract contract TestHelper is Test, WellDeployer { snapshot.reserves = well.getReserves(); } - function checkInvariant(address _well) internal { + function checkInvariant(address _well) internal view { uint256[] memory _reserves = IWell(_well).getReserves(); Call memory _wellFunction = IWell(_well).wellFunction(); assertLe( IERC20(_well).totalSupply(), - IWellFunction(_wellFunction.target).calcLpTokenSupply(_reserves, _wellFunction.data) + IWellFunction(_wellFunction.target).calcLpTokenSupply(_reserves, _wellFunction.data), + "totalSupply() is greater than calcLpTokenSupply()" + ); + } + + function checkStableSwapInvariant(address _well) internal view { + uint256[] memory _reserves = IWell(_well).getReserves(); + Call memory _wellFunction = IWell(_well).wellFunction(); + assertApproxEqAbs( + IERC20(_well).totalSupply(), + IWellFunction(_wellFunction.target).calcLpTokenSupply(_reserves, _wellFunction.data), + 2 ); } diff --git a/test/Well.Skim.t.sol b/test/Well.Skim.t.sol index d3179128..4c6d32f8 100644 --- a/test/Well.Skim.t.sol +++ b/test/Well.Skim.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {TestHelper, Balances} from "test/TestHelper.sol"; -contract WellSkimTest is TestHelper { +contract WellSkimStableSwapTest is TestHelper { function setUp() public { setupWell(2); } diff --git a/test/WellUpgradeable.t.sol b/test/WellUpgradeable.t.sol new file mode 100644 index 00000000..e94ee079 --- /dev/null +++ b/test/WellUpgradeable.t.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test, console} from "forge-std/Test.sol"; +import {WellUpgradeable} from "src/WellUpgradeable.sol"; +import {IERC20} from "test/TestHelper.sol"; +import {WellDeployer} from "script/helpers/WellDeployer.sol"; +import {MockPump} from "mocks/pumps/MockPump.sol"; +import {Well, Call, IWellFunction, IPump, IERC20} from "src/Well.sol"; +import {ConstantProduct2} from "src/functions/ConstantProduct2.sol"; +import {Aquifer} from "src/Aquifer.sol"; +import {WellDeployer} from "script/helpers/WellDeployer.sol"; +import {LibWellUpgradeableConstructor} from "src/libraries/LibWellUpgradeableConstructor.sol"; +import {MockToken} from "mocks/tokens/MockToken.sol"; +import {WellDeployer} from "script/helpers/WellDeployer.sol"; +import {ERC1967Proxy} from "oz/proxy/ERC1967/ERC1967Proxy.sol"; +import {MockWellUpgradeable} from "mocks/wells/MockWellUpgradeable.sol"; + +contract WellUpgradeTest is Test, WellDeployer { + address proxyAddress; + address aquifer; + address initialOwner; + address user; + address mockPumpAddress; + address wellFunctionAddress; + address token1Address; + address token2Address; + address wellAddress; + address wellImplementation; + IERC20[] tokens = new IERC20[](2); + + function setUp() public { + // Tokens + 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"); + token2Address = address(tokens[1]); + vm.label(token2Address, "token2"); + + user = makeAddr("user"); + + // Mint tokens + MockToken(address(tokens[0])).mint(user, 10_000_000_000_000_000); + MockToken(address(tokens[1])).mint(user, 10_000_000_000_000_000); + // Well Function + IWellFunction cp2 = new ConstantProduct2(); + vm.label(address(cp2), "CP2"); + wellFunctionAddress = address(cp2); + Call memory wellFunction = Call(address(cp2), abi.encode("beanstalkFunction")); + + // Pump + IPump mockPump = new MockPump(); + mockPumpAddress = address(mockPump); + vm.label(mockPumpAddress, "mockPump"); + Call[] memory pumps = new Call[](1); + // init new mock pump with "beanstalk" data + pumps[0] = Call(address(mockPump), abi.encode("beanstalkPump")); + aquifer = address(new Aquifer()); + vm.label(aquifer, "aquifer"); + wellImplementation = address(new WellUpgradeable()); + vm.label(wellImplementation, "wellImplementation"); + initialOwner = makeAddr("owner"); + + // Well + 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 + // We encode and bore a well upgradeable from the aquifer + // 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 + // 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 + // (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. + // This will typically be an encoded function call, and allows initializing + // the storage of the proxy like a Solidity constructor. + + // Deploy Proxy + vm.startPrank(initialOwner); + ERC1967Proxy proxy = new ERC1967Proxy( + address(well), // implementation address + LibWellUpgradeableConstructor.encodeWellInitFunctionCall(tokens, wellFunction) // init data + ); + vm.stopPrank(); + proxyAddress = address(proxy); + vm.label(proxyAddress, "proxyAddress"); + + vm.startPrank(user); + tokens[0].approve(wellAddress, type(uint256).max); + tokens[1].approve(wellAddress, type(uint256).max); + tokens[0].approve(proxyAddress, type(uint256).max); + tokens[1].approve(proxyAddress, type(uint256).max); + vm.stopPrank(); + } + + ///////////////////// Storage Tests ///////////////////// + + function testProxyGetAquifer() public { + assertEq(address(aquifer), WellUpgradeable(proxyAddress).aquifer()); + } + + function testProxyGetPump() public { + Call[] memory proxyPumps = WellUpgradeable(proxyAddress).pumps(); + assertEq(mockPumpAddress, proxyPumps[0].target); + // this passes but why? Pump data are supposed + // to be stored in the implementation storage from the borewell call + assertEq(abi.encode("beanstalkPump"), proxyPumps[0].data); + } + + function testProxyGetTokens() public { + IERC20[] memory proxyTokens = WellUpgradeable(proxyAddress).tokens(); + assertEq(address(proxyTokens[0]), token1Address); + assertEq(address(proxyTokens[1]), token2Address); + } + + function testProxyGetWellFunction() public { + Call memory proxyWellFunction = WellUpgradeable(proxyAddress).wellFunction(); + assertEq(address(proxyWellFunction.target), address(wellFunctionAddress)); + assertEq(proxyWellFunction.data, abi.encode("beanstalkFunction")); + } + + function testProxyGetSymbolInStorage() public { + assertEq("BEANWETHCP2uw", WellUpgradeable(proxyAddress).symbol()); + } + + function testProxyInitVersion() public { + uint256 expectedVersion = 1; + assertEq(expectedVersion, WellUpgradeable(proxyAddress).getVersion()); + } + + function testProxyNumTokens() public { + uint256 expectedNumTokens = 2; + assertEq(expectedNumTokens, WellUpgradeable(proxyAddress).numberOfTokens()); + } + + ///////////////// Interaction test ////////////////// + + function testProxyAddLiquidity() public { + vm.startPrank(user); + 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); + assertEq(amounts, WellUpgradeable(proxyAddress).getReserves()); + vm.stopPrank(); + } + + ////////////// Ownership Tests ////////////// + + function testProxyOwner() public { + assertEq(initialOwner, WellUpgradeable(proxyAddress).owner()); + } + + function testProxyTransferOwnership() public { + vm.prank(initialOwner); + address newOwner = makeAddr("newOwner"); + WellUpgradeable(proxyAddress).transferOwnership(newOwner); + assertEq(newOwner, WellUpgradeable(proxyAddress).owner()); + } + + function testRevertTransferOwnershipFromNotOnwer() public { + address notOwner = makeAddr("notOwner"); + vm.prank(notOwner); + vm.expectRevert(); + WellUpgradeable(proxyAddress).transferOwnership(notOwner); + } + + ////////////////////// Upgrade Tests ////////////////////// + + function testUpgradeToNewImplementation() 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"); + 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(1, MockWellUpgradeable(proxyAddress).getVersion()); + 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 new file mode 100644 index 00000000..9d0395e4 --- /dev/null +++ b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {console, TestHelper, IERC20} from "test/TestHelper.sol"; +import {Stable2} from "src/functions/Stable2.sol"; +import {IBeanstalkWellFunction} from "src/interfaces/IBeanstalkWellFunction.sol"; +import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; + +/// @dev Tests the {Stable2.CalcReserveAtRatioLiquidity} Well function directly. +contract BeanstalkStable2LiquidityTest is TestHelper { + IBeanstalkWellFunction _f; + bytes data; + + //////////// SETUP //////////// + + function setUp() public { + address lut = address(new Stable2LUT1()); + _f = new Stable2(lut); + deployMockTokens(2); + data = abi.encode(18, 18); + } + + function test_calcReserveAtRatioLiquidity_equal_equal() public view { + uint256[] memory reserves = new uint256[](2); + reserves[0] = 100e18; + reserves[1] = 100e18; + uint256[] memory ratios = new uint256[](2); + ratios[0] = 1; + ratios[1] = 1; + + uint256 reserve0 = _f.calcReserveAtRatioLiquidity(reserves, 0, ratios, data); + uint256 reserve1 = _f.calcReserveAtRatioLiquidity(reserves, 1, ratios, data); + + assertApproxEqRel(reserve0, 100.002494212050875384e18, 0.0003e18); + assertApproxEqRel(reserve1, 100.002494212050875384e18, 0.0003e18); + } + + function test_calcReserveAtRatioLiquidity_equal_diff() public view { + uint256[] memory reserves = new uint256[](2); + reserves[0] = 50e18; + reserves[1] = 100e18; + uint256[] memory ratios = new uint256[](2); + ratios[0] = 1; + ratios[1] = 1; + + uint256 reserve0 = _f.calcReserveAtRatioLiquidity(reserves, 0, ratios, data); + uint256 reserve1 = _f.calcReserveAtRatioLiquidity(reserves, 1, ratios, data); + + assertApproxEqRel(reserve0, 100.002494212050875384e18, 0.0003e18); + assertApproxEqRel(reserve1, 50.001091026498328056e18, 0.0003e18); + } + + function test_calcReserveAtRatioLiquidity_diff_equal() public view { + uint256[] memory reserves = new uint256[](2); + reserves[0] = 1e18; + reserves[1] = 1e18; + uint256[] memory ratios = new uint256[](2); + ratios[0] = 2; + ratios[1] = 1; + + uint256 reserve0 = _f.calcReserveAtRatioLiquidity(reserves, 0, ratios, data); + uint256 reserve1 = _f.calcReserveAtRatioLiquidity(reserves, 1, ratios, data); + + assertApproxEqRel(reserve0, 4.576236561359714812e18, 0.0001e18); + assertApproxEqRel(reserve1, 0.21852354514449462e18, 0.0001e18); + } + + function test_calcReserveAtRatioLiquidity_diff_diff() public view { + uint256[] memory reserves = new uint256[](2); + reserves[0] = 2e18; + reserves[1] = 1e18; + uint256[] memory ratios = new uint256[](2); + ratios[0] = 12; + ratios[1] = 10; + + uint256 reserve0 = _f.calcReserveAtRatioLiquidity(reserves, 0, ratios, data); + uint256 reserve1 = _f.calcReserveAtRatioLiquidity(reserves, 1, ratios, data); + + assertApproxEqRel(reserve0, 1.685591553208758586e18, 0.0004e18); + assertApproxEqRel(reserve1, 1.18623685249742594e18, 0.0004e18); + } + + function test_calcReserveAtRatioLiquidity_fuzz(uint256[2] memory reserves, uint256[2] memory ratios) public view { + 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); + ratios[i] = bound(ratios[i], 1e18, 4e18); + } + + // create 2 new reserves, one where reserve[0] is updated, and one where reserve[1] is updated. + uint256[] memory r0Updated = new uint256[](2); + r0Updated[1] = reserves[1]; + uint256[] memory r1Updated = new uint256[](2); + r1Updated[0] = reserves[0]; + for (uint256 i; i < 2; ++i) { + uint256 reserve = _f.calcReserveAtRatioLiquidity(uint2ToUintN(reserves), i, uint2ToUintN(ratios), data); + // update reserves. + if (i == 0) { + r0Updated[0] = reserve; + } else { + r1Updated[1] = reserve; + } + } + + { + uint256 targetPrice = ratios[0] * 1e6 / ratios[1]; + uint256 reservePrice0 = _f.calcRate(r0Updated, 0, 1, data); + uint256 reservePrice1 = _f.calcRate(r1Updated, 0, 1, data); + + // 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.0005e18, "reservePrice0 <> reservePrice1"); + } + } + + function test_calcReserveAtRatioLiquidity_invalidJ() public { + uint256[] memory reserves = new uint256[](2); + uint256[] memory ratios = new uint256[](2); + 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 new file mode 100644 index 00000000..a21d80fc --- /dev/null +++ b/test/beanstalk/BeanstalkStable2.calcReserveAtRatioSwap.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {console, TestHelper, IERC20} from "test/TestHelper.sol"; +import {Stable2} from "src/functions/Stable2.sol"; +import {IBeanstalkWellFunction} from "src/interfaces/IBeanstalkWellFunction.sol"; +import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; + +/// @dev Tests the {Stable2} Well function directly. +contract BeanstalkStable2SwapTest is TestHelper { + IBeanstalkWellFunction _f; + bytes data; + + //////////// SETUP //////////// + + function setUp() public { + address lut = address(new Stable2LUT1()); + _f = new Stable2(lut); + data = abi.encode(18, 18); + } + + function test_calcReserveAtRatioSwap_equal_equal() public view { + uint256[] memory reserves = new uint256[](2); + reserves[0] = 100e18; + reserves[1] = 100e18; + uint256[] memory ratios = new uint256[](2); + ratios[0] = 1; + ratios[1] = 1; + + uint256 reserve0 = _f.calcReserveAtRatioSwap(reserves, 0, ratios, data); + uint256 reserve1 = _f.calcReserveAtRatioSwap(reserves, 1, ratios, data); + + assertEq(reserve0, 99.999921040536083478e18); + assertEq(reserve1, 99.999921040536083478e18); + } + + function test_calcReserveAtRatioSwap_equal_diff() public view { + uint256[] memory reserves = new uint256[](2); + reserves[0] = 50e18; + reserves[1] = 100e18; + uint256[] memory ratios = new uint256[](2); + ratios[0] = 1; + ratios[1] = 1; + + uint256 reserve0 = _f.calcReserveAtRatioSwap(reserves, 0, ratios, data); + uint256 reserve1 = _f.calcReserveAtRatioSwap(reserves, 1, ratios, data); + + assertEq(reserve0, 73.513867858788351572e18); + assertEq(reserve1, 73.513867858788351572e18); + } + + function test_calcReserveAtRatioSwap_diff_equal() public view { + uint256[] memory reserves = new uint256[](2); + reserves[0] = 100e18; + reserves[1] = 100e18; + uint256[] memory ratios = new uint256[](2); + ratios[0] = 2; + ratios[1] = 1; + + uint256 reserve0 = _f.calcReserveAtRatioSwap(reserves, 0, ratios, data); + uint256 reserve1 = _f.calcReserveAtRatioSwap(reserves, 1, ratios, data); + + assertEq(reserve0, 180.644064978044534737e18); // 180.644064978044534737e18, 100e18 + assertEq(reserve1, 39.474244037189430513e18); // 100e18, 39.475055811844664131e18 + } + + function test_calcReserveAtRatioSwap_diff_diff() public view { + uint256[] memory reserves = new uint256[](2); + reserves[0] = 90e18; + reserves[1] = 110e18; + uint256[] memory ratios = new uint256[](2); + ratios[0] = 110; + ratios[1] = 90; + + uint256 reserve0 = _f.calcReserveAtRatioSwap(reserves, 0, ratios, data); + uint256 reserve1 = _f.calcReserveAtRatioSwap(reserves, 1, ratios, data); + + assertEq(reserve0, 129.268187496764805614e18); // 129.268187496764805614e18, 90e18 + assertEq(reserve1, 73.11634314279891828e18); // 110e18, 73.116252343760233529e18 + } + + function test_calcReserveAtRatioSwap_fuzz(uint256[2] memory reserves, uint256[2] memory ratios) public view { + for (uint256 i; i < 2; ++i) { + // Upper bound is limited by stableSwap, + // due to the stableswap reserves being extremely far apart. + + 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); + } + + // create 2 new reserves, one where reserve[0] is updated, and one where reserve[1] is updated. + uint256[] memory updatedReserves = new uint256[](2); + for (uint256 i; i < 2; ++i) { + updatedReserves[i] = _f.calcReserveAtRatioSwap(uint2ToUintN(reserves), i, uint2ToUintN(ratios), data); + } + + { + uint256 targetPrice = ratios[0] * 1e6 / ratios[1]; + uint256 reservePrice0 = _f.calcRate(updatedReserves, 0, 1, data); + + // estimated price and actual price are within 0.015% in the worst case. + 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 new file mode 100644 index 00000000..eeb9c731 --- /dev/null +++ b/test/functions/Stable2.t.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {console, TestHelper, IERC20} from "test/TestHelper.sol"; +import {WellFunctionHelper, IMultiFlowPumpWellFunction} from "./WellFunctionHelper.sol"; +import {Stable2} from "src/functions/Stable2.sol"; +import {Stable2LUT1} from "src/functions/StableLUT/Stable2LUT1.sol"; + +/// @dev Tests the {Stable2} Well function directly. +contract Stable2Test is WellFunctionHelper { + /** + * State A: Same decimals + * D (lpTokenSupply) should be the summation of + * the reserves, assuming they are equal. + */ + uint256 STATE_A_B0 = 10 * 1e18; + uint256 STATE_A_B1 = 10 * 1e18; + uint256 STATE_A_LP = 20 * 1e18; + + /** + * State B: Different decimals + * @notice the stableswap implmentation + * uses precision-adjusted to 18 decimals. + * In other words, a token with 6 decimals + * will be scaled up such that it uses 18 decimals. + * + * @dev D is the summation of the reserves, + * assuming they are equal. + * + */ + uint256 STATE_B_B0 = 10 * 1e18; + uint256 STATE_B_B1 = 20 * 1e6; + uint256 STATE_B_LP = 29_405_570_361_996_060_057; // ~29.4e18 + + /// State C: Similar decimals + uint256 STATE_C_B0 = 20 * 1e12; + uint256 STATE_C_B1 = 25 * 1e18; + uint256 STATE_C_LP = 44_906_735_116_816_626_495; // 44.9e18 + + /// @dev See {calcLpTokenSupply}. + uint256 MAX_RESERVE = 1e32; + + //////////// SETUP //////////// + + function setUp() public { + IERC20[] memory _tokens = deployMockTokens(2); + tokens = _tokens; + address lut = address(new Stable2LUT1()); + _function = IMultiFlowPumpWellFunction(new Stable2(lut)); + } + + function test_metadata() public view { + assertEq(_function.name(), "Stable2"); + assertEq(_function.symbol(), "S2"); + } + + //////////// LP TOKEN SUPPLY //////////// + + /// @dev calcLpTokenSupply: same decimals, manual calc for 2 equal reserves + function test_calcLpTokenSupply_sameDecimals() public { + _data = abi.encode(18, 18); + uint256[] memory reserves = new uint256[](2); + reserves[0] = STATE_A_B0; + reserves[1] = STATE_A_B1; + assertEq(_function.calcLpTokenSupply(reserves, _data), STATE_A_LP); + } + + /// @dev calcLpTokenSupply: diff decimals + function test_calcLpTokenSupply_diffDecimals() public { + uint256[] memory reserves = new uint256[](2); + _data = abi.encode(18, 6); + reserves[0] = STATE_B_B0; // 10 USDT + reserves[1] = STATE_B_B1; // 20 BEAN + assertEq(_function.calcLpTokenSupply(reserves, _data), STATE_B_LP); + } + + //////////// RESERVES //////////// + + /// @dev calcReserve: same decimals, both positions + /// Matches example in {testLpTokenSupplySameDecimals}. + function test_calcReserve_sameDecimals() public { + uint256[] memory reserves = new uint256[](2); + + /// STATE A + // find reserves[0] + _data = abi.encode(18, 18); + reserves[0] = 0; + reserves[1] = STATE_A_B1; + assertEq(_function.calcReserve(reserves, 0, STATE_A_LP, _data), STATE_A_B0); + + // find reserves[1] + reserves[0] = STATE_A_B0; + reserves[1] = 0; + assertEq(_function.calcReserve(reserves, 1, STATE_A_LP, _data), STATE_A_B1); + + /// STATE C + // find reserves[1] + _data = abi.encode(12, 18); + reserves[0] = STATE_C_B0; + reserves[1] = 0; + assertEq(_function.calcReserve(reserves, 1, STATE_C_LP, _data), STATE_C_B1); + } + + /// @dev calcReserve: diff decimals, both positions + /// Matches example in {testLpTokenSupplyDiffDecimals}. + function test_calcReserve_diffDecimals() public { + _data = abi.encode(18, 6); + uint256[] memory reserves = new uint256[](2); + + /// STATE B + // find reserves[0] + reserves[0] = 0; + reserves[1] = STATE_B_B1; + assertEq(_function.calcReserve(reserves, 0, STATE_B_LP, _data), STATE_B_B0); + + // find reserves[1] + reserves[0] = STATE_B_B0; + reserves[1] = 0; + assertEq(_function.calcReserve(reserves, 1, STATE_B_LP, _data), STATE_B_B1); + } + + //////////// LP TOKEN SUPPLY //////////// + + /// @dev invariant: reserves -> lpTokenSupply -> reserves should match + function testFuzz_calcLpTokenSupply(uint256[2] memory _reserves) public { + _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/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); + for (uint256 i = 0; i < reserves.length; ++i) { + assertEq(reserves[i], underlying[i], "reserves mismatch"); + } + } + + //////////// FUZZ //////////// + + 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/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); + + uint256 lpTokenSupply = _function.calcLpTokenSupply(reserves, _data); + uint256 reserve0 = _function.calcReserve(reserves, 0, lpTokenSupply, _data); + uint256 reserve1 = _function.calcReserve(reserves, 1, lpTokenSupply, _data); + + if (reserves[0] < 1e12) { + assertApproxEqAbs(reserve0, reserves[0], 1); + } else { + assertApproxEqRel(reserve0, reserves[0], 3e6); + } + if (reserves[1] < 1e12) { + assertApproxEqAbs(reserve1, reserves[1], 1); + } else { + assertApproxEqRel(reserve1, reserves[1], 3e6); + } + } + + ///////// CALC RATE /////// + + function test_calcRateStable() public { + _data = abi.encode(18, 18); + uint256[] memory reserves = new uint256[](2); + reserves[0] = 1e18; + reserves[1] = 1e18; + assertEq(_function.calcRate(reserves, 0, 1, _data), 1e6); + assertEq(_function.calcRate(reserves, 1, 0, _data), 1e6); + } + + function test_calcRateStable6Decimals() public { + _data = abi.encode(18, 6); + uint256[] memory reserves = new uint256[](2); + reserves[0] = 100e18; + reserves[1] = 100e6; + assertEq(_function.calcRate(reserves, 1, 0, _data), 1e6); + assertEq(_function.calcRate(reserves, 0, 1, _data), 1e6); + } +} diff --git a/test/integration/IntegrationTestHelper.sol b/test/integration/IntegrationTestHelper.sol index b4731187..6abc0920 100644 --- a/test/integration/IntegrationTestHelper.sol +++ b/test/integration/IntegrationTestHelper.sol @@ -4,12 +4,11 @@ pragma solidity ^0.8.20; import {Test, console, stdError} from "forge-std/Test.sol"; import {Well, Call, IERC20} from "src/Well.sol"; import {Aquifer} from "src/Aquifer.sol"; -import {ConstantProduct2} from "src/functions/ConstantProduct2.sol"; import {IWellFunction} from "src/interfaces/IWellFunction.sol"; import {MultiFlowPump} from "src/pumps/MultiFlowPump.sol"; import {LibContractInfo} from "src/libraries/LibContractInfo.sol"; import {Users} from "test/helpers/Users.sol"; -import {TestHelper, Balances} from "test/TestHelper.sol"; +import {TestHelper, Balances, ConstantProduct2} from "test/TestHelper.sol"; import {from18, to18} from "test/pumps/PumpHelpers.sol"; abstract contract IntegrationTestHelper is TestHelper { @@ -106,13 +105,14 @@ abstract contract IntegrationTestHelper is TestHelper { uint256 pasteIndex ) internal pure returns (bytes memory stuff) { uint256 clipboardData; - clipboardData = clipboardData | uint256(_type) << 248; + clipboardData = clipboardData | (uint256(_type) << 248); - clipboardData = clipboardData | returnDataIndex << 160 | (copyIndex * 32) + 32 << 80 | (pasteIndex * 32) + 36; + clipboardData = + clipboardData | (returnDataIndex << 160) | (((copyIndex * 32) + 32) << 80) | ((pasteIndex * 32) + 36); if (useEther) { // put 0x1 in second byte // shift left 30 bytes - clipboardData = clipboardData | 1 << 240; + clipboardData = clipboardData | (1 << 240); return abi.encodePacked(clipboardData, amount); } else { return abi.encodePacked(clipboardData); diff --git a/test/integration/interfaces/ICurve.sol b/test/integration/interfaces/ICurve.sol new file mode 100644 index 00000000..ef0850b6 --- /dev/null +++ b/test/integration/interfaces/ICurve.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma experimental ABIEncoderV2; +pragma solidity ^0.8.20; + +interface ICurvePool { + function A_precise() external view returns (uint256); + + function get_balances() external view returns (uint256[2] memory); + + function totalSupply() external view returns (uint256); + + function add_liquidity(uint256[2] memory amounts, uint256 min_mint_amount) external returns (uint256); + + function remove_liquidity_one_coin( + uint256 _token_amount, + int128 i, + uint256 min_amount + ) external returns (uint256); + + function balances(int128 i) external view returns (uint256); + + function fee() external view returns (uint256); + + function coins(uint256 i) external view returns (address); + + function get_virtual_price() external view returns (uint256); + + function calc_token_amount(uint256[2] calldata amounts, bool deposit) external view returns (uint256); + + function calc_withdraw_one_coin(uint256 _token_amount, int128 i) external view returns (uint256); + + function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external returns (uint256); + + function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy) external returns (uint256); + + function transfer(address recipient, uint256 amount) external returns (bool); +} + +interface ICurveZap { + function add_liquidity( + address _pool, + uint256[4] memory _deposit_amounts, + uint256 _min_mint_amount + ) external returns (uint256); + + function calc_token_amount( + address _pool, + uint256[4] memory _amounts, + bool _is_deposit + ) external returns (uint256); +} + +interface ICurvePoolR { + function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256); + + function exchange_underlying( + int128 i, + int128 j, + uint256 dx, + uint256 min_dy, + address receiver + ) external returns (uint256); + + function remove_liquidity_one_coin( + uint256 _token_amount, + int128 i, + uint256 min_amount, + address receiver + ) external returns (uint256); +} + +interface ICurvePool2R { + function add_liquidity( + uint256[2] memory amounts, + uint256 min_mint_amount, + address reciever + ) external returns (uint256); + + function remove_liquidity( + uint256 _burn_amount, + uint256[2] memory _min_amounts, + address reciever + ) external returns (uint256[2] calldata); + + function remove_liquidity_imbalance( + uint256[2] memory _amounts, + uint256 _max_burn_amount, + address reciever + ) external returns (uint256); +} + +interface ICurvePool3R { + function add_liquidity( + uint256[3] memory amounts, + uint256 min_mint_amount, + address reciever + ) external returns (uint256); + + function remove_liquidity( + uint256 _burn_amount, + uint256[3] memory _min_amounts, + address reciever + ) external returns (uint256[3] calldata); + + function remove_liquidity_imbalance( + uint256[3] memory _amounts, + uint256 _max_burn_amount, + address reciever + ) external returns (uint256); +} + +interface ICurvePool4R { + function add_liquidity( + uint256[4] memory amounts, + uint256 min_mint_amount, + address reciever + ) external returns (uint256); + + function remove_liquidity( + uint256 _burn_amount, + uint256[4] memory _min_amounts, + address reciever + ) external returns (uint256[4] calldata); + + function remove_liquidity_imbalance( + uint256[4] memory _amounts, + uint256 _max_burn_amount, + address reciever + ) external returns (uint256); +} + +interface I3Curve { + function get_virtual_price() external view returns (uint256); +} + +interface ICurveFactory { + function get_coins(address _pool) external view returns (address[4] calldata); + + function get_underlying_coins(address _pool) external view returns (address[8] calldata); +} + +interface ICurveCryptoFactory { + function get_coins(address _pool) external view returns (address[8] calldata); +} + +interface ICurvePoolC { + function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external returns (uint256); +} + +interface ICurvePoolNoReturn { + function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external; + + function add_liquidity(uint256[3] memory amounts, uint256 min_mint_amount) external; + + function remove_liquidity(uint256 _burn_amount, uint256[3] memory _min_amounts) external; + + function remove_liquidity_imbalance(uint256[3] memory _amounts, uint256 _max_burn_amount) external; + + function remove_liquidity_one_coin(uint256 _token_amount, uint256 i, uint256 min_amount) external; +} + +interface ICurvePoolNoReturn128 { + function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external; + + function remove_liquidity_one_coin(uint256 _token_amount, int128 i, uint256 min_amount) external; +} diff --git a/test/libraries/LibMath.t.sol b/test/libraries/LibMath.t.sol index 30c1ca4f..2ef8086f 100644 --- a/test/libraries/LibMath.t.sol +++ b/test/libraries/LibMath.t.sol @@ -47,39 +47,39 @@ contract LibMathTest is TestHelper { //////////// SQRT //////////// /// @dev zero case - function testSqrt0() public { + function testSqrt0() public pure { assertEq(LibMath.sqrt(0), 0); } /// @dev perfect square case, small number - function testSqrtPerfectSmall() public { + function testSqrtPerfectSmall() public pure { assertEq(LibMath.sqrt(4), 2); } /// @dev perfect square case, large number /// 4e6 = sqrt(1.6e13) - function testSqrtPerfectLarge() public { + function testSqrtPerfectLarge() public pure { assertEq(LibMath.sqrt(16 * 1e12), 4 * 1e6); } /// @dev imperfect square case, small number with decimal < 0.5 - function testSqrtImperfectSmallLt() public { + function testSqrtImperfectSmallLt() public pure { assertEq(LibMath.sqrt(2), 1); // rounds down from 1.414... } /// @dev imperfect square case, large number with decimal < 0.5 - function testSqrtImperfectLargeLt() public { + function testSqrtImperfectLargeLt() public pure { assertEq(LibMath.sqrt(1250 * 1e6), 35_355); // rounds down from 35355.339... } /// @dev imperfect square case, small number with decimal >= 0.5 - function testSqrtImperfectSmallGte() public { + function testSqrtImperfectSmallGte() public pure { assertEq(LibMath.sqrt(3), 1); // rounds down from 1.732... } /// @dev imperfect square case, small number with decimal >= 0.5 /// 2828427124 = sqrt(8e18) - function testSqrtImperfectLargeGte() public { + function testSqrtImperfectLargeGte() public pure { assertEq(LibMath.sqrt(8 * 1e18), 2_828_427_124); // rounds down from 2.828...e9 } @@ -89,7 +89,7 @@ contract LibMathTest is TestHelper { LibMath.roundUpDiv(1, 0); } - function test_roundUpDiv() public { + function test_roundUpDiv() public pure { assertEq(LibMath.roundUpDiv(1, 3), 1); assertEq(LibMath.roundUpDiv(1, 2), 1); assertEq(LibMath.roundUpDiv(2, 3), 1); diff --git a/test/libraries/TestABDK.t.sol b/test/libraries/TestABDK.t.sol index 4fd6b602..ea741c4d 100644 --- a/test/libraries/TestABDK.t.sol +++ b/test/libraries/TestABDK.t.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "test/TestHelper.sol"; @@ -67,12 +68,12 @@ contract ABDKTest is TestHelper { return a.fromUInt().div(b.fromUInt()).powu(c); } - function testFuzz_FromUIntToLog2(uint256 x) public { + function testFuzz_FromUIntToLog2(uint256 x) public pure { x = bound(x, 1, type(uint256).max); assertEq(ABDKMathQuad.fromUInt(x).log_2(), ABDKMathQuad.fromUIntToLog2(x)); } - function testFuzz_pow_2ToUInt(uint256 x) public { + function testFuzz_pow_2ToUInt(uint256 x) public pure { x = bound(x, 0, 255); // test the pow_2ToUInt function diff --git a/test/pumps/Pump.Update.t.sol b/test/pumps/Pump.Update.t.sol index acc5f4c3..30a0d6b5 100644 --- a/test/pumps/Pump.Update.t.sol +++ b/test/pumps/Pump.Update.t.sol @@ -156,23 +156,15 @@ contract PumpUpdateTest is TestHelper { mWell.update(address(pump), b, data); increaseTime(CAP_INTERVAL); - - console.log(1); - console.log(4); uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), data); - console.log(4); - console.log("EMA Reserves Length: %s", emaReserves.length); assertEq(emaReserves.length, 2); assertApproxEqAbs(emaReserves[0], 1_156_587, 1); // = 2^(log2(1000000) * 0.9^12 +log2(1224743) * (1-0.9^12)) assertApproxEqAbs(emaReserves[1], 1_729_223, 1); // = 2^(log2(2000000) * 0.9^12 +log2(1632992) * (1-0.9^12)) - console.log(3); bytes16[] memory cumulativeReserves = abi.decode(pump.readCumulativeReserves(address(mWell), data), (bytes16[])); assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1_224_743, 1); assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1_632_992, 1); - console.log(4); - (uint256[] memory twaReserves, bytes memory twaCumulativeReservesBytes) = pump.readTwaReserves(address(mWell), startCumulativeReserves, block.timestamp - CAP_INTERVAL, data);