diff --git a/.gitignore b/.gitignore index 160db549..e3a12683 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ stash lcov.info .DS_Store test/output/ + +# Python +__pycache__ \ No newline at end of file diff --git a/mocks/wells/MockReserveWell.sol b/mocks/wells/MockReserveWell.sol index ea8036e0..046b0504 100644 --- a/mocks/wells/MockReserveWell.sol +++ b/mocks/wells/MockReserveWell.sol @@ -5,17 +5,28 @@ pragma solidity ^0.8.20; import {IPump} from "src/interfaces/pumps/IPump.sol"; +import {Call} from "src/interfaces/IWell.sol"; /** * @notice Mock Well that allows setting of reserves. */ contract MockReserveWell { uint256[] reserves; + Call _wellFunction; + constructor() { reserves = new uint256[](2); } + function setWellFunction(Call calldata __wellFunction) external { + _wellFunction = __wellFunction; + } + + function wellFunction() external view returns (Call memory) { + return _wellFunction; + } + function setReserves(uint256[] memory _reserves) public { reserves = _reserves; } diff --git a/package.json b/package.json index 1dc14c29..f0699859 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@beanstalk/wells", - "version": "0.4.0", + "version": "1.1.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/src/functions/ConstantProduct.sol b/src/functions/ConstantProduct.sol index abb136d5..087e5801 100644 --- a/src/functions/ConstantProduct.sol +++ b/src/functions/ConstantProduct.sol @@ -23,6 +23,8 @@ import {LibMath} from "src/libraries/LibMath.sol"; contract ConstantProduct is ProportionalLPToken, IBeanstalkWellFunction { using LibMath for uint256; + uint256 constant CALC_RATE_PRECISION = 1e18; + /// @dev `s = π(b_i)^(1/n) * n` function calcLpTokenSupply( uint256[] calldata reserves, @@ -92,4 +94,13 @@ contract ConstantProduct is ProportionalLPToken, IBeanstalkWellFunction { } reserve /= reserves.length - 1; } + + function calcRate( + uint256[] calldata reserves, + uint256 i, + uint256 j, + bytes calldata + ) external pure returns (uint256 rate) { + return reserves[i] * CALC_RATE_PRECISION / reserves[j]; + } } diff --git a/src/functions/ConstantProduct2.sol b/src/functions/ConstantProduct2.sol index 9e771089..543ce07f 100644 --- a/src/functions/ConstantProduct2.sol +++ b/src/functions/ConstantProduct2.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.20; import {IBeanstalkWellFunction} from "src/interfaces/IBeanstalkWellFunction.sol"; import {ProportionalLPToken2} from "src/functions/ProportionalLPToken2.sol"; import {LibMath} from "src/libraries/LibMath.sol"; +import {Math} from "oz/utils/math/Math.sol"; /** * @title ConstantProduct2 @@ -18,9 +19,10 @@ import {LibMath} from "src/libraries/LibMath.sol"; * `b_i` is the reserve at index `i` */ contract ConstantProduct2 is ProportionalLPToken2, IBeanstalkWellFunction { - using LibMath for uint256; + using Math for uint256; uint256 constant EXP_PRECISION = 1e12; + uint256 constant CALC_RATE_PRECISION = 1e18; /** * @dev `s = (b_0 * b_1)^(1/2)` @@ -103,4 +105,13 @@ contract ConstantProduct2 is ProportionalLPToken2, IBeanstalkWellFunction { uint256 i = j == 1 ? 0 : 1; reserve = reserves[i] * ratios[j] / ratios[i]; } + + function calcRate( + uint256[] calldata reserves, + uint256 i, + uint256 j, + bytes calldata + ) external pure returns (uint256 rate) { + return reserves[i] * CALC_RATE_PRECISION / reserves[j]; + } } diff --git a/src/functions/ProportionalLPToken.sol b/src/functions/ProportionalLPToken.sol index ffe54bba..346439f3 100644 --- a/src/functions/ProportionalLPToken.sol +++ b/src/functions/ProportionalLPToken.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; import {IWellFunction} from "src/interfaces/IWellFunction.sol"; +import {Math} from "oz/utils/math/Math.sol"; /** * @title ProportionalLPToken @@ -12,6 +13,8 @@ import {IWellFunction} from "src/interfaces/IWellFunction.sol"; * recieves `s * b_i / S` of each underlying token. */ abstract contract ProportionalLPToken is IWellFunction { + using Math for uint256; + function calcLPTokenUnderlying( uint256 lpTokenAmount, uint256[] calldata reserves, @@ -20,7 +23,7 @@ abstract contract ProportionalLPToken is IWellFunction { ) external pure returns (uint256[] memory underlyingAmounts) { underlyingAmounts = new uint256[](reserves.length); for (uint256 i; i < reserves.length; ++i) { - underlyingAmounts[i] = lpTokenAmount * reserves[i] / lpTokenSupply; + underlyingAmounts[i] = lpTokenAmount.mulDiv(reserves[i], lpTokenSupply); } } } diff --git a/src/functions/ProportionalLPToken2.sol b/src/functions/ProportionalLPToken2.sol index 0a814ab2..729f34ae 100644 --- a/src/functions/ProportionalLPToken2.sol +++ b/src/functions/ProportionalLPToken2.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; import {IWellFunction} from "src/interfaces/IWellFunction.sol"; +import {Math} from "oz/utils/math/Math.sol"; /** * @title ProportionalLPToken2 @@ -12,6 +13,8 @@ import {IWellFunction} from "src/interfaces/IWellFunction.sol"; * recieves `s * b_i / S` of each underlying token. */ abstract contract ProportionalLPToken2 is IWellFunction { + using Math for uint256; + function calcLPTokenUnderlying( uint256 lpTokenAmount, uint256[] calldata reserves, @@ -19,7 +22,7 @@ abstract contract ProportionalLPToken2 is IWellFunction { bytes calldata ) external pure returns (uint256[] memory underlyingAmounts) { underlyingAmounts = new uint256[](2); - underlyingAmounts[0] = lpTokenAmount * reserves[0] / lpTokenSupply; - underlyingAmounts[1] = lpTokenAmount * reserves[1] / lpTokenSupply; + underlyingAmounts[0] = lpTokenAmount.mulDiv(reserves[0], lpTokenSupply); + underlyingAmounts[1] = lpTokenAmount.mulDiv(reserves[1], lpTokenSupply); } } diff --git a/src/interfaces/IBeanstalkWellFunction.sol b/src/interfaces/IBeanstalkWellFunction.sol index 9d95877a..09c541b2 100644 --- a/src/interfaces/IBeanstalkWellFunction.sol +++ b/src/interfaces/IBeanstalkWellFunction.sol @@ -2,34 +2,18 @@ pragma solidity ^0.8.20; -import {IWellFunction} from "src/interfaces/IWellFunction.sol"; +import {IMultiFlowPumpWellFunction} from "src/interfaces/IMultiFlowPumpWellFunction.sol"; /** * @title IBeanstalkWellFunction * @notice Defines all necessary functions for Beanstalk to support a Well Function in addition to functions defined in the primary interface. - * This includes 2 functions to solve for a given reserve value suc that the average price between + * It extends `IMultiFlowPumpWellFunction` as Beanstalk requires Wells to use MultiFlowPump in order to have access to manipulation resistant oracles. + * Beanstalk requires 2 functions to solve for a given reserve value such that the average price between * the given reserve and all other reserves equals the average of the input ratios. - * `calcReserveAtRatioSwap` assumes the target ratios are reached through executing a swap. - * `calcReserveAtRatioLiquidity` assumes the target ratios are reached through adding/removing liquidity. + * - `calcReserveAtRatioSwap` assumes the target ratios are reached through executing a swap. Note: `calcReserveAtRatioSwap` is included in {IMultiFlowPumpWellFunction}. + * - `calcReserveAtRatioLiquidity` assumes the target ratios are reached through adding/removing liquidity. */ -interface IBeanstalkWellFunction is IWellFunction { - /** - * @notice Calculates the `j` reserve such that `π_{i | i != j} (d reserves_j / d reserves_i) = π_{i | i != j}(ratios_j / ratios_i)`. - * assumes that reserve_j is being swapped for other reserves in the Well. - * @dev used by Beanstalk to calculate the deltaB every Season - * @param reserves The reserves of the Well - * @param j The index of the reserve to solve for - * @param ratios The ratios of reserves to solve for - * @param data Well function data provided on every call - * @return reserve The resulting reserve at the jth index - */ - function calcReserveAtRatioSwap( - uint256[] calldata reserves, - uint256 j, - uint256[] calldata ratios, - bytes calldata data - ) external view returns (uint256 reserve); - +interface IBeanstalkWellFunction is IMultiFlowPumpWellFunction { /** * @notice Calculates the `j` reserve such that `π_{i | i != j} (d reserves_j / d reserves_i) = π_{i | i != j}(ratios_j / ratios_i)`. * assumes that reserve_j is being added or removed in exchange for LP Tokens. diff --git a/src/interfaces/IMultiFlowPumpWellFunction.sol b/src/interfaces/IMultiFlowPumpWellFunction.sol new file mode 100644 index 00000000..5a067219 --- /dev/null +++ b/src/interfaces/IMultiFlowPumpWellFunction.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IWellFunction} from "src/interfaces/IWellFunction.sol"; + +/** + * @title IMultiFlowPumpWellFunction + * @dev A Well Function must implement IMultiFlowPumpWellFunction to be supported by + * the Multi Flow Pump. + */ +interface IMultiFlowPumpWellFunction is IWellFunction { + /** + * @notice Calculates the `j` reserve such that `π_{i | i != j} (d reserves_j / d reserves_i) = π_{i | i != j}(ratios_j / ratios_i)`. + * assumes that reserve_j is being swapped for other reserves in the Well. + * @dev used by Beanstalk to calculate the deltaB every Season + * @param reserves The reserves of the Well + * @param j The index of the reserve to solve for + * @param ratios The ratios of reserves to solve for + * @param data Well function data provided on every call + * @return reserve The resulting reserve at the jth index + */ + function calcReserveAtRatioSwap( + uint256[] calldata reserves, + uint256 j, + uint256[] calldata ratios, + bytes calldata data + ) external view returns (uint256 reserve); + + /** + * @notice Calculates the rate at which j can be exchanged for i. + * @param reserves The reserves of the Well + * @param i The index of the token for which the output is being calculated + * @param j The index of the token for which 1 token is being exchanged + * @param data Well function data provided on every call + * @return rate The rate at which j can be exchanged for i + * @dev should return with 36 decimal precision + */ + function calcRate( + uint256[] calldata reserves, + uint256 i, + uint256 j, + bytes calldata data + ) external view returns (uint256 rate); +} diff --git a/src/interfaces/pumps/IMultiFlowPumpErrors.sol b/src/interfaces/pumps/IMultiFlowPumpErrors.sol index 918e6700..ae605354 100644 --- a/src/interfaces/pumps/IMultiFlowPumpErrors.sol +++ b/src/interfaces/pumps/IMultiFlowPumpErrors.sol @@ -11,4 +11,6 @@ interface IMultiFlowPumpErrors { error NotInitialized(); error NoTimePassed(); + + error TooManyTokens(); } diff --git a/src/libraries/LibLastReserveBytes.sol b/src/libraries/LibLastReserveBytes.sol index c89e7d8e..d0ba80ca 100644 --- a/src/libraries/LibLastReserveBytes.sol +++ b/src/libraries/LibLastReserveBytes.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.20; +import {ABDKMathQuad} from "src/libraries/ABDKMathQuad.sol"; + /** * @title LibLastReserveBytes * @author Publius @@ -15,15 +17,25 @@ pragma solidity ^0.8.20; * in reserves for manipulation resistance purposes, the gas savings is worth the lose of precision. */ library LibLastReserveBytes { + using ABDKMathQuad for uint256; + using ABDKMathQuad for bytes16; + function readNumberOfReserves(bytes32 slot) internal view returns (uint8 _numberOfReserves) { assembly { _numberOfReserves := shr(248, sload(slot)) } } - function storeLastReserves(bytes32 slot, uint40 lastTimestamp, bytes16[] memory reserves) internal { + function storeLastReserves(bytes32 slot, uint40 lastTimestamp, uint256[] memory lastReserves) internal { // Potential optimization – shift reserve bytes left to perserve extra decimal precision. - uint8 n = uint8(reserves.length); + uint8 n = uint8(lastReserves.length); + + bytes16[] memory reserves = new bytes16[](n); + + for (uint256 i; i < n; ++i) { + reserves[i] = lastReserves[i].fromUInt(); + } + if (n == 1) { assembly { sstore(slot, or(or(shl(208, lastTimestamp), shl(248, n)), shl(104, shr(152, mload(add(reserves, 32)))))) @@ -73,7 +85,7 @@ library LibLastReserveBytes { function readLastReserves(bytes32 slot) internal view - returns (uint8 n, uint40 lastTimestamp, bytes16[] memory reserves) + returns (uint8 n, uint40 lastTimestamp, uint256[] memory lastReserves) { // Shortcut: two reserves can be quickly unpacked from one slot bytes32 temp; @@ -82,13 +94,17 @@ library LibLastReserveBytes { n := shr(248, temp) lastTimestamp := shr(208, temp) } - if (n == 0) return (n, lastTimestamp, reserves); + if (n == 0) return (n, lastTimestamp, lastReserves); // Initialize array with length `n`, fill it in via assembly - reserves = new bytes16[](n); + bytes16[] memory reserves = new bytes16[](n); assembly { mstore(add(reserves, 32), shl(152, shr(104, temp))) } - if (n == 1) return (n, lastTimestamp, reserves); + if (n == 1) { + lastReserves = new uint256[](1); + lastReserves[0] = reserves[0].toUInt(); + return (n, lastTimestamp, lastReserves); + } assembly { mstore(add(reserves, 64), shl(152, temp)) } @@ -116,6 +132,11 @@ library LibLastReserveBytes { } } } + + lastReserves = new uint256[](n); + for (uint256 i; i < n; ++i) { + lastReserves[i] = reserves[i].toUInt(); + } } function readBytes(bytes32 slot) internal view returns (bytes32 value) { diff --git a/src/libraries/LibMath.sol b/src/libraries/LibMath.sol index cb0bfd69..c291c69f 100644 --- a/src/libraries/LibMath.sol +++ b/src/libraries/LibMath.sol @@ -68,7 +68,6 @@ library LibMath { * Implementation from: https://github.com/Gaussian-Process/solidity-sqrt/blob/main/src/FixedPointMathLib.sol * based on https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol */ - function sqrt(uint256 a) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { @@ -134,100 +133,88 @@ library LibMath { } } - /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 - /// @param a The multiplicand - /// @param b The multiplier - /// @param denominator The divisor - /// @return result The 256-bit result - /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv - function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) { - // 512-bit multiply [prod1 prod0] = a * b - // Compute the product mod 2**256 and mod 2**256 - 1 - // then use the Chinese Remainder Theorem to reconstruct - // the 512 bit result. The result is stored in two 256 - // variables such that product = prod1 * 2**256 + prod0 - uint256 prod0; // Least significant 256 bits of the product - uint256 prod1; // Most significant 256 bits of the product - assembly { - let mm := mulmod(a, b, not(0)) - prod0 := mul(a, b) - prod1 := sub(sub(mm, prod0), lt(mm, prod0)) - } - - // Handle non-overflow cases, 256 by 256 division - if (prod1 == 0) { - require(denominator > 0); + /** + * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 + * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) + * with further edits by Uniswap Labs also under MIT license. + */ + function mulDivOrMax(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { + unchecked { + // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use + // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2^256 + prod0. + uint256 prod0; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product assembly { - result := div(prod0, denominator) + let mm := mulmod(x, y, not(0)) + prod0 := mul(x, y) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) } - return result; - } - // Make sure the result is less than 2**256. - // Also prevents denominator == 0 - require(denominator > prod1); + // Handle non-overflow cases, 256 by 256 division. + if (prod1 == 0) { + // Solidity will revert if denominator == 0, unlike the div opcode on its own. + // The surrounding unchecked block does not change this fact. + // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. + return prod0 / denominator; + } - /////////////////////////////////////////////// - // 512 by 256 division. - /////////////////////////////////////////////// + // Make sure the result is less than 2^256. Also prevents denominator == 0. + if (denominator <= prod1) return type(uint256).max; - // Make division exact by subtracting the remainder from [prod1 prod0] - // Compute remainder using mulmod - uint256 remainder; - assembly { - remainder := mulmod(a, b, denominator) - } - // Subtract 256 bit number from 512 bit number - assembly { - prod1 := sub(prod1, gt(remainder, prod0)) - prod0 := sub(prod0, remainder) - } + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// - // Factor powers of two out of denominator - // Compute largest power of two divisor of denominator. - // Always >= 1. - unchecked { - uint256 twos = (type(uint256).max - denominator + 1) & denominator; - // Divide denominator by power of two + // Make division exact by subtracting the remainder from [prod1 prod0]. + uint256 remainder; assembly { - denominator := div(denominator, twos) + // Compute remainder using mulmod. + remainder := mulmod(x, y, denominator) + + // Subtract 256 bit number from 512 bit number. + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) } - // Divide [prod1 prod0] by the factors of two + // Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1. + // See https://cs.stackexchange.com/q/138556/92363. + + // Does not overflow because the denominator cannot be zero at this stage in the function. + uint256 twos = denominator & (~denominator + 1); assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide [prod1 prod0] by twos. prod0 := div(prod0, twos) - } - // Shift in bits from prod1 into prod0. For this we need - // to flip `twos` such that it is 2**256 / twos. - // If twos is zero, then it becomes one - assembly { + + // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one. twos := add(div(sub(0, twos), twos), 1) } + + // Shift in bits from prod1 into prod0. prod0 |= prod1 * twos; - // Invert denominator mod 2**256 - // Now that denominator is an odd number, it has an inverse - // modulo 2**256 such that denominator * inv = 1 mod 2**256. - // Compute the inverse by starting with a seed that is correct - // correct for four bits. That is, denominator * inv = 1 mod 2**4 - uint256 inv = (3 * denominator) ^ 2; - // Now use Newton-Raphson iteration to improve the precision. - // Thanks to Hensel's lifting lemma, this also works in modular - // arithmetic, doubling the correct bits in each step. - inv *= 2 - denominator * inv; // inverse mod 2**8 - inv *= 2 - denominator * inv; // inverse mod 2**16 - inv *= 2 - denominator * inv; // inverse mod 2**32 - inv *= 2 - denominator * inv; // inverse mod 2**64 - inv *= 2 - denominator * inv; // inverse mod 2**128 - inv *= 2 - denominator * inv; // inverse mod 2**256 - - // Because the division is now exact we can divide by multiplying - // with the modular inverse of denominator. This will give us the - // correct result modulo 2**256. Since the precoditions guarantee - // that the outcome is less than 2**256, this is the final result. - // We don't need to compute the high bits of the result and prod1 + // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such + // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for + // four bits. That is, denominator * inv = 1 mod 2^4. + uint256 inverse = (3 * denominator) ^ 2; + + // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works + // in modular arithmetic, doubling the correct bits in each step. + inverse *= 2 - denominator * inverse; // inverse mod 2^8 + inverse *= 2 - denominator * inverse; // inverse mod 2^16 + inverse *= 2 - denominator * inverse; // inverse mod 2^32 + inverse *= 2 - denominator * inverse; // inverse mod 2^64 + inverse *= 2 - denominator * inverse; // inverse mod 2^128 + inverse *= 2 - denominator * inverse; // inverse mod 2^256 + + // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. + // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is + // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1 // is no longer required. - result = prod0 * inv; + result = prod0 * inverse; return result; } } diff --git a/src/pumps/MultiFlowPump.sol b/src/pumps/MultiFlowPump.sol index 57062df0..3d975a31 100644 --- a/src/pumps/MultiFlowPump.sol +++ b/src/pumps/MultiFlowPump.sol @@ -4,12 +4,16 @@ pragma solidity ^0.8.20; import {IPump} from "src/interfaces/pumps/IPump.sol"; import {IMultiFlowPumpErrors} from "src/interfaces/pumps/IMultiFlowPumpErrors.sol"; -import {IWell} from "src/interfaces/IWell.sol"; +import {IWell, Call} from "src/interfaces/IWell.sol"; import {IInstantaneousPump} from "src/interfaces/pumps/IInstantaneousPump.sol"; +import {IMultiFlowPumpWellFunction} from "src/interfaces/IMultiFlowPumpWellFunction.sol"; import {ICumulativePump} from "src/interfaces/pumps/ICumulativePump.sol"; import {ABDKMathQuad} from "src/libraries/ABDKMathQuad.sol"; import {LibBytes16} from "src/libraries/LibBytes16.sol"; import {LibLastReserveBytes} from "src/libraries/LibLastReserveBytes.sol"; +import {Math} from "oz/utils/math/Math.sol"; +import {SafeCast} from "oz/utils/math/SafeCast.sol"; +import {LibMath} from "src/libraries/LibMath.sol"; /** * @title MultiFlowPump @@ -30,44 +34,48 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu using LibBytes16 for bytes32; using ABDKMathQuad for bytes16; using ABDKMathQuad for uint256; + using SafeCast for int256; + using Math for uint256; + using LibMath for uint256; - bytes16 public immutable LOG_MAX_INCREASE; - bytes16 public immutable LOG_MAX_DECREASE; - bytes16 public immutable ALPHA; - uint256 public immutable CAP_INTERVAL; + uint256 constant CAP_PRECISION = 1e18; + uint256 constant CAP_PRECISION2 = 2 ** 128; + bytes16 constant MAX_CONVERT_TO_128x128 = 0x407dffffffffffffffffffffffffffff; + uint256 constant MAX_UINT256_SQRT = 340_282_366_920_938_463_463_374_607_431_768_211_455; struct PumpState { uint40 lastTimestamp; - bytes16[] lastReserves; + uint256[] lastReserves; bytes16[] emaReserves; bytes16[] cumulativeReserves; } - /** - * @param _maxPercentIncrease The maximum percent increase allowed in a single block. Must be in quadruple precision format (See {ABDKMathQuad}). - * @param _maxPercentDecrease The maximum percent decrease allowed in a single block. Must be in quadruple precision format (See {ABDKMathQuad}). - * @param _capInterval How often to increase the magnitude of the cap on the change in reserve in seconds. - * @param _alpha The geometric EMA constant. Must be in quadruple precision format (See {ABDKMathQuad}). - * - * @dev The Pump will not flow and should definitely be considered invalid if the following constraints are not met: - * - 0% < _maxPercentIncrease - * - 0% < _maxPercentDecrease <= 100% - * - 0 < ALPHA <= 1 - * - _capInterval > 0 - * The above constraints are not checked in the constructor for gas efficiency reasons. - * When evaluating the manipulation resistance of an instance of a Multi Flow Pump for use as an oracle, stricter - * constraints should be used. - */ - constructor(bytes16 _maxPercentIncrease, bytes16 _maxPercentDecrease, uint256 _capInterval, bytes16 _alpha) { - LOG_MAX_INCREASE = ABDKMathQuad.ONE.add(_maxPercentIncrease).log_2(); - LOG_MAX_DECREASE = ABDKMathQuad.ONE.sub(_maxPercentDecrease).log_2(); - CAP_INTERVAL = _capInterval; - ALPHA = _alpha; + struct CapReservesParameters { + bytes16[][] maxRateChanges; + bytes16 maxLpSupplyIncrease; + bytes16 maxLpSupplyDecrease; + } + + struct CapRatesVariables { + uint256 r; + uint256 rLast; + uint256 rLimit; + uint256[] ratios; } //////////////////// PUMP //////////////////// - function update(uint256[] calldata reserves, bytes calldata) external { + /** + * @dev Update the Pump's manipulation resistant reserve balances for a given `well` with `reserves`. + */ + function update(uint256[] calldata reserves, bytes calldata data) external { + // Require two token well + if (reserves.length != 2) { + revert TooManyTokens(); + } + + (bytes16 alpha, uint256 capInterval, CapReservesParameters memory crp) = + abi.decode(data, (bytes16, uint256, CapReservesParameters)); uint256 numberOfReserves = reserves.length; PumpState memory pumpState; @@ -85,18 +93,20 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu bytes16 alphaN; bytes16 deltaTimestampBytes; - bytes16 capExponent; + uint256 capExponent; // Isolate in brackets to prevent stack too deep errors { uint256 deltaTimestamp = _getDeltaTimestamp(pumpState.lastTimestamp); // If no time has passed, don't update the pump reserves. if (deltaTimestamp == 0) return; - alphaN = ALPHA.powu(deltaTimestamp); + alphaN = alpha.powu(deltaTimestamp); deltaTimestampBytes = deltaTimestamp.fromUInt(); - // Round up in case CAP_INTERVAL > block time to guarantee capExponent > 0 if time has passed since the last update. - capExponent = ((deltaTimestamp - 1) / CAP_INTERVAL + 1).fromUInt(); + // Round up in case capInterval > block time to guarantee capExponent > 0 if time has passed since the last update. + capExponent = calcCapExponent(deltaTimestamp, capInterval); } + pumpState.lastReserves = _capReserves(msg.sender, pumpState.lastReserves, reserves, capExponent, crp); + // Read: Cumulative & EMA Reserves // Start at the slot after `pumpState.lastReserves` uint256 numSlots = _getSlotsOffset(numberOfReserves); @@ -109,16 +119,12 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu } pumpState.cumulativeReserves = slot.readBytes16(numberOfReserves); - uint256 _reserve; + bytes16 lastReserve; for (uint256 i; i < numberOfReserves; ++i) { - // Use a minimum of 1 for reserve. Geometric means will be set to 0 if a reserve is 0. - _reserve = reserves[i]; - pumpState.lastReserves[i] = - _capReserve(pumpState.lastReserves[i], (_reserve > 0 ? _reserve : 1).fromUIntToLog2(), capExponent); + lastReserve = pumpState.lastReserves[i].fromUIntToLog2(); pumpState.emaReserves[i] = - pumpState.lastReserves[i].mul((ABDKMathQuad.ONE.sub(alphaN))).add(pumpState.emaReserves[i].mul(alphaN)); - pumpState.cumulativeReserves[i] = - pumpState.cumulativeReserves[i].add(pumpState.lastReserves[i].mul(deltaTimestampBytes)); + lastReserve.mul((ABDKMathQuad.ONE.sub(alphaN))).add(pumpState.emaReserves[i].mul(alphaN)); + pumpState.cumulativeReserves[i] = pumpState.cumulativeReserves[i].add(lastReserve.mul(deltaTimestampBytes)); } // Write: Cumulative & EMA Reserves @@ -153,7 +159,7 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu } // Write: Last Timestamp & Last Reserves - slot.storeLastReserves(lastTimestamp, byteReserves); + slot.storeLastReserves(lastTimestamp, reserves); // Write: EMA Reserves // Start at the slot after `byteReserves` @@ -169,83 +175,165 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu /** * @dev Reads the last capped reserves from the Pump from storage. */ - function readLastCappedReserves(address well) public view returns (uint256[] memory lastCappedReserves) { - (uint8 numberOfReserves,, bytes16[] memory lastReserves) = _getSlotForAddress(well).readLastReserves(); + function readLastCappedReserves( + address well, + bytes memory + ) public view returns (uint256[] memory lastCappedReserves) { + uint8 numberOfReserves; + (numberOfReserves,, lastCappedReserves) = _getSlotForAddress(well).readLastReserves(); if (numberOfReserves == 0) { revert NotInitialized(); } - lastCappedReserves = new uint256[](numberOfReserves); - for (uint256 i; i < numberOfReserves; ++i) { - lastCappedReserves[i] = lastReserves[i].pow_2ToUInt(); - } } /** * @dev Reads the capped reserves from the Pump updated to the current block using the current reserves of `well`. */ - function readCappedReserves(address well) external view returns (uint256[] memory cappedReserves) { + function readCappedReserves( + address well, + bytes calldata data + ) external view returns (uint256[] memory cappedReserves) { + (, uint256 capInterval, CapReservesParameters memory crp) = + abi.decode(data, (bytes16, uint256, CapReservesParameters)); bytes32 slot = _getSlotForAddress(well); uint256[] memory currentReserves = IWell(well).getReserves(); - (uint8 numberOfReserves, uint40 lastTimestamp, bytes16[] memory lastReserves) = slot.readLastReserves(); + uint8 numberOfReserves; + uint40 lastTimestamp; + (numberOfReserves, lastTimestamp, cappedReserves) = slot.readLastReserves(); if (numberOfReserves == 0) { revert NotInitialized(); } uint256 deltaTimestamp = _getDeltaTimestamp(lastTimestamp); - cappedReserves = new uint256[](numberOfReserves); if (deltaTimestamp == 0) { - for (uint256 i; i < numberOfReserves; ++i) { - cappedReserves[i] = lastReserves[i].pow_2ToUInt(); - } return cappedReserves; } - bytes16 capExponent = ((deltaTimestamp - 1) / CAP_INTERVAL + 1).fromUInt(); + uint256 capExponent = calcCapExponent(deltaTimestamp, capInterval); + cappedReserves = _capReserves(well, cappedReserves, currentReserves, capExponent, crp); + } - for (uint256 i; i < numberOfReserves; ++i) { - cappedReserves[i] = - _capReserve(lastReserves[i], currentReserves[i].fromUIntToLog2(), capExponent).pow_2ToUInt(); + /** + * @notice Cap `reserves` to have at most a maximum % increase/decrease in rate and a maximum % increase/decrease in total liquidity + * in relation to `lastReserves` based on the parameters defined in `crp` and the time passed since the last update, which is used + * to calculate `capExponent`. + * @param well The address of the Well + * @param lastReserves The last capped reserves. + * @param reserves The current reserves being capped. + * @param capExponent The exponent to raise the all % changes to. + * @param crp The parameters for capping reserves. See {CapReservesParameters}. + * @return cappedReserves The current reserves capped to the maximum % changes defined by `crp`. + */ + function _capReserves( + address well, + uint256[] memory lastReserves, + uint256[] memory reserves, + uint256 capExponent, + CapReservesParameters memory crp + ) internal view returns (uint256[] memory cappedReserves) { + Call memory wf = IWell(well).wellFunction(); + IMultiFlowPumpWellFunction mfpWf = IMultiFlowPumpWellFunction(wf.target); + + // The order that the LP token supply and the rates are capped are dependent upon the values of the reserves to maximize precision. + cappedReserves = _capLpTokenSupply(lastReserves, reserves, capExponent, crp, mfpWf, wf.data, true); + + // If `_capLpTokenSupply` returns an empty array, then the rates should be capped first. + if (cappedReserves.length == 0) { + cappedReserves = _capRates(lastReserves, reserves, capExponent, crp, mfpWf, wf.data); + + cappedReserves = _capLpTokenSupply(lastReserves, cappedReserves, capExponent, crp, mfpWf, wf.data, false); + } else { + cappedReserves = _capRates(lastReserves, cappedReserves, capExponent, crp, mfpWf, wf.data); } } /** - * @dev Adds a cap to the reserve value to prevent extreme changes. - * - * Linear space: - * max reserve = (last reserve) * ((1 + MAX_PERCENT_CHANGE_PER_BLOCK) ^ capExponent) - * - * Log space: - * log2(max reserve) = log2(last reserve) + capExponent*log2(1 + MAX_PERCENT_CHANGE_PER_BLOCK) - * - * `bytes16 lastReserve` <- log2(last reserve) - * `bytes16 capExponent` <- cap exponent - * `bytes16 LOG_MAX_INCREASE` <- log2(1 + MAX_PERCENT_CHANGE_PER_BLOCK) - * - * ∴ `maxReserve = lastReserve + capExponent*LOG_MAX_INCREASE` - * + * @dev Cap the change in ratio of `reserves` to a maximum % change from `lastReserves`. */ - function _capReserve( - bytes16 lastReserve, - bytes16 reserve, - bytes16 capExponent - ) internal view returns (bytes16 cappedReserve) { - // Reserve decreasing (lastReserve > reserve) - if (lastReserve.cmp(reserve) == 1) { - bytes16 minReserve = lastReserve.add(capExponent.mul(LOG_MAX_DECREASE)); - // if reserve < minimum reserve, set reserve to minimum reserve - if (minReserve.cmp(reserve) == 1) reserve = minReserve; - } - // Reserve increasing or staying the same (lastReserve <= reserve) - else { - bytes16 maxReserve = lastReserve.add(capExponent.mul(LOG_MAX_INCREASE)); - // If reserve > maximum reserve, set reserve to maximum reserve - if (reserve.cmp(maxReserve) == 1) reserve = maxReserve; - } - cappedReserve = reserve; + function _capRates( + uint256[] memory lastReserves, + uint256[] memory reserves, + uint256 capExponent, + CapReservesParameters memory crp, + IMultiFlowPumpWellFunction mfpWf, + bytes memory data + ) internal view returns (uint256[] memory cappedReserves) { + cappedReserves = reserves; + // Part 1: Cap Rates + // Use the larger reserve as the numerator for the ratio to maximize precision + (uint256 i, uint256 j) = lastReserves[0] > lastReserves[1] ? (0, 1) : (1, 0); + CapRatesVariables memory crv; + crv.rLast = mfpWf.calcRate(lastReserves, i, j, data); + crv.r = mfpWf.calcRate(cappedReserves, i, j, data); + + // If the ratio increased, check that it didn't increase above the max. + if (crv.r > crv.rLast) { + bytes16 tempExp = ABDKMathQuad.ONE.add(crp.maxRateChanges[i][j]).powu(capExponent); + crv.rLimit = tempExp.cmp(MAX_CONVERT_TO_128x128) != -1 + ? crv.rLimit = type(uint256).max + : crv.rLast.mulDivOrMax(tempExp.to128x128().toUint256(), CAP_PRECISION2); + if (crv.r > crv.rLimit) { + calcReservesAtRatioSwap(mfpWf, crv.rLimit, cappedReserves, i, j, data); + } + // If the ratio decreased, check that it didn't overflow during calculation + } else if (crv.r < crv.rLast) { + bytes16 tempExp = ABDKMathQuad.ONE.div(ABDKMathQuad.ONE.add(crp.maxRateChanges[j][i])).powu(capExponent); + // Check for overflow before converting to 128x128 + if (tempExp.cmp(MAX_CONVERT_TO_128x128) != -1) { + crv.rLimit = 0; // Set limit to 0 in case of overflow + } else { + crv.rLimit = crv.rLast.mulDiv(tempExp.to128x128().toUint256(), CAP_PRECISION2); + } + if (crv.r < crv.rLimit) { + calcReservesAtRatioSwap(mfpWf, crv.rLimit, cappedReserves, i, j, data); + } + } + } + + /** + * @dev Cap the change in LP Token Supply of `reserves` to a maximum % change from `lastReserves`. + */ + function _capLpTokenSupply( + uint256[] memory lastReserves, + uint256[] memory reserves, + uint256 capExponent, + CapReservesParameters memory crp, + IMultiFlowPumpWellFunction mfpWf, + bytes memory data, + bool returnIfBelowMin + ) internal view returns (uint256[] memory cappedReserves) { + cappedReserves = reserves; + // Part 2: Cap LP Token Supply Change + uint256 lastLpTokenSupply = tryCalcLpTokenSupply(mfpWf, lastReserves, data); + uint256 lpTokenSupply = tryCalcLpTokenSupply(mfpWf, cappedReserves, data); + + // If LP Token Supply increased, check that it didn't increase above the max. + if (lpTokenSupply > lastLpTokenSupply) { + bytes16 tempExp = ABDKMathQuad.ONE.add(crp.maxLpSupplyIncrease).powu(capExponent); + uint256 maxLpTokenSupply = tempExp.cmp(MAX_CONVERT_TO_128x128) != -1 + ? type(uint256).max + : lastLpTokenSupply.mulDiv(tempExp.to128x128().toUint256(), CAP_PRECISION2); + + if (lpTokenSupply > maxLpTokenSupply) { + // If `_capLpTokenSupply` decreases the reserves, cap the ratio first, to maximize precision. + if (returnIfBelowMin) return new uint256[](0); + cappedReserves = tryCalcLPTokenUnderlying(mfpWf, maxLpTokenSupply, cappedReserves, lpTokenSupply, data); + } + // If LP Token Suppply decreased, check that it didn't increase below the min. + } else if (lpTokenSupply < lastLpTokenSupply) { + uint256 minLpTokenSupply = lastLpTokenSupply + * (ABDKMathQuad.ONE.sub(crp.maxLpSupplyDecrease)).powu(capExponent).to128x128().toUint256() / CAP_PRECISION2; + if (lpTokenSupply < minLpTokenSupply) { + cappedReserves = tryCalcLPTokenUnderlying(mfpWf, minLpTokenSupply, cappedReserves, lpTokenSupply, data); + } + } } //////////////////// EMA RESERVES //////////////////// - function readLastInstantaneousReserves(address well) external view returns (uint256[] memory emaReserves) { + function readLastInstantaneousReserves( + address well, + bytes memory + ) external view returns (uint256[] memory emaReserves) { bytes32 slot = _getSlotForAddress(well); uint8 numberOfReserves = slot.readNumberOfReserves(); if (numberOfReserves == 0) { @@ -264,11 +352,13 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu function readInstantaneousReserves( address well, - bytes memory + bytes memory data ) external view returns (uint256[] memory emaReserves) { + (bytes16 alpha, uint256 capInterval, CapReservesParameters memory crp) = + abi.decode(data, (bytes16, uint256, CapReservesParameters)); bytes32 slot = _getSlotForAddress(well); uint256[] memory reserves = IWell(well).getReserves(); - (uint8 numberOfReserves, uint40 lastTimestamp, bytes16[] memory lastReserves) = slot.readLastReserves(); + (uint8 numberOfReserves, uint40 lastTimestamp, uint256[] memory lastReserves) = slot.readLastReserves(); if (numberOfReserves == 0) { revert NotInitialized(); } @@ -277,8 +367,8 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu slot := add(slot, offset) } bytes16[] memory lastEmaReserves = slot.readBytes16(numberOfReserves); - uint256 deltaTimestamp = _getDeltaTimestamp(lastTimestamp); emaReserves = new uint256[](numberOfReserves); + uint256 deltaTimestamp = _getDeltaTimestamp(lastTimestamp); // If no time has passed, return last EMA reserves. if (deltaTimestamp == 0) { for (uint256 i; i < numberOfReserves; ++i) { @@ -286,12 +376,13 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu } return emaReserves; } - bytes16 capExponent = ((deltaTimestamp - 1) / CAP_INTERVAL + 1).fromUInt(); - bytes16 alphaN = ALPHA.powu(deltaTimestamp); + uint256 capExponent = calcCapExponent(deltaTimestamp, capInterval); + lastReserves = _capReserves(well, lastReserves, reserves, capExponent, crp); + bytes16 alphaN = alpha.powu(deltaTimestamp); for (uint256 i; i < numberOfReserves; ++i) { - lastReserves[i] = _capReserve(lastReserves[i], reserves[i].fromUIntToLog2(), capExponent); - emaReserves[i] = - lastReserves[i].mul((ABDKMathQuad.ONE.sub(alphaN))).add(lastEmaReserves[i].mul(alphaN)).pow_2ToUInt(); + emaReserves[i] = lastReserves[i].fromUIntToLog2().mul((ABDKMathQuad.ONE.sub(alphaN))).add( + lastEmaReserves[i].mul(alphaN) + ).pow_2ToUInt(); } } @@ -300,7 +391,10 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu /** * @notice Read the latest cumulative reserves of `well`. */ - function readLastCumulativeReserves(address well) external view returns (bytes16[] memory cumulativeReserves) { + function readLastCumulativeReserves( + address well, + bytes memory + ) external view returns (bytes16[] memory cumulativeReserves) { bytes32 slot = _getSlotForAddress(well); uint8 numberOfReserves = slot.readNumberOfReserves(); if (numberOfReserves == 0) { @@ -315,16 +409,21 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu function readCumulativeReserves( address well, - bytes memory + bytes memory data ) external view returns (bytes memory cumulativeReserves) { - bytes16[] memory byteCumulativeReserves = _readCumulativeReserves(well); + bytes16[] memory byteCumulativeReserves = _readCumulativeReserves(well, data); cumulativeReserves = abi.encode(byteCumulativeReserves); } - function _readCumulativeReserves(address well) internal view returns (bytes16[] memory cumulativeReserves) { + function _readCumulativeReserves( + address well, + bytes memory data + ) internal view returns (bytes16[] memory cumulativeReserves) { + (, uint256 capInterval, CapReservesParameters memory crp) = + abi.decode(data, (bytes16, uint256, CapReservesParameters)); bytes32 slot = _getSlotForAddress(well); uint256[] memory reserves = IWell(well).getReserves(); - (uint8 numberOfReserves, uint40 lastTimestamp, bytes16[] memory lastReserves) = slot.readLastReserves(); + (uint8 numberOfReserves, uint40 lastTimestamp, uint256[] memory lastReserves) = slot.readLastReserves(); if (numberOfReserves == 0) { revert NotInitialized(); } @@ -339,11 +438,11 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu return cumulativeReserves; } bytes16 deltaTimestampBytes = deltaTimestamp.fromUInt(); - bytes16 capExponent = ((deltaTimestamp - 1) / CAP_INTERVAL + 1).fromUInt(); + uint256 capExponent = calcCapExponent(deltaTimestamp, capInterval); + lastReserves = _capReserves(well, lastReserves, reserves, capExponent, crp); // Currently, there is so support for overflow. for (uint256 i; i < cumulativeReserves.length; ++i) { - lastReserves[i] = _capReserve(lastReserves[i], reserves[i].fromUIntToLog2(), capExponent); - cumulativeReserves[i] = cumulativeReserves[i].add(lastReserves[i].mul(deltaTimestampBytes)); + cumulativeReserves[i] = cumulativeReserves[i].add(lastReserves[i].fromUIntToLog2().mul(deltaTimestampBytes)); } } @@ -351,9 +450,9 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu address well, bytes calldata startCumulativeReserves, uint256 startTimestamp, - bytes memory + bytes memory data ) public view returns (uint256[] memory twaReserves, bytes memory cumulativeReserves) { - bytes16[] memory byteCumulativeReserves = _readCumulativeReserves(well); + bytes16[] memory byteCumulativeReserves = _readCumulativeReserves(well, data); bytes16[] memory byteStartCumulativeReserves = abi.decode(startCumulativeReserves, (bytes16[])); twaReserves = new uint256[](byteCumulativeReserves.length); @@ -372,6 +471,34 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu //////////////////// HELPERS //////////////////// + /** + * @dev Calculate the cap exponent for a given `deltaTimestamp` and `capInterval`. + */ + function calcCapExponent(uint256 deltaTimestamp, uint256 capInterval) private pure returns (uint256 capExponent) { + capExponent = ((deltaTimestamp - 1) / capInterval + 1); + } + + /** + * @dev Calculates the capped reserves given a rate limit. + */ + function calcReservesAtRatioSwap( + IMultiFlowPumpWellFunction mfpWf, + uint256 rLimit, + uint256[] memory reserves, + uint256 i, + uint256 j, + bytes memory data + ) private view returns (uint256[] memory) { + uint256[] memory ratios = new uint256[](2); + ratios[i] = rLimit; + ratios[j] = CAP_PRECISION; + // Use a minimum of 1 for reserve. Geometric means will be set to 0 if a reserve is 0. + uint256 cappedReserveI = Math.max(tryCalcReserveAtRatioSwap(mfpWf, reserves, i, ratios, data), 1); + reserves[j] = Math.max(tryCalcReserveAtRatioSwap(mfpWf, reserves, j, ratios, data), 1); + reserves[i] = cappedReserveI; + return reserves; + } + /** * @dev Convert an `address` into a `bytes32` by zero padding the right 12 bytes. */ @@ -380,7 +507,8 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu } /** - * @dev Get the starting byte of the slot that contains the `n`th element of an array. + * @dev Get the slot number that contains the `n`th element of an array. + * slots are seperated by 32 bytes to allow for future expansion of the Pump (i.e supporting Well with more than 3 tokens). */ function _getSlotsOffset(uint256 numberOfReserves) internal pure returns (uint256 _slotsOffset) { _slotsOffset = ((numberOfReserves - 1) / 2 + 1) << 5; @@ -392,4 +520,67 @@ contract MultiFlowPump is IPump, IMultiFlowPumpErrors, IInstantaneousPump, ICumu function _getDeltaTimestamp(uint40 lastTimestamp) internal view returns (uint256 _deltaTimestamp) { return uint256(uint40(block.timestamp) - lastTimestamp); } + + /** + * @dev Assumes that if `calcReserveAtRatioSwap` fails, it fails because of overflow. + * If the call fails, returns the maximum possible return value for `calcReserveAtRatioSwap`. + */ + function tryCalcReserveAtRatioSwap( + IMultiFlowPumpWellFunction wf, + uint256[] memory reserves, + uint256 i, + uint256[] memory ratios, + bytes memory data + ) internal view returns (uint256 reserve) { + try wf.calcReserveAtRatioSwap(reserves, i, ratios, data) returns (uint256 _reserve) { + reserve = _reserve; + } catch { + reserve = type(uint256).max; + } + } + + /** + * @dev Assumes that if `calcLpTokenSupply` fails, it fails because of overflow. + * If it fails, returns the maximum possible return value for `calcLpTokenSupply`. + */ + function tryCalcLpTokenSupply( + IMultiFlowPumpWellFunction wf, + uint256[] memory reserves, + bytes memory data + ) internal view returns (uint256 lpTokenSupply) { + try wf.calcLpTokenSupply(reserves, data) returns (uint256 _lpTokenSupply) { + lpTokenSupply = _lpTokenSupply; + } catch { + lpTokenSupply = MAX_UINT256_SQRT; + } + } + + /** + * @dev Assumes that if `calcLPTokenUnderlying` fails, it fails because of overflow. + * If the call fails, returns the maximum possible return value for `calcLPTokenUnderlying`. + * Also, enforces a minimum of 1 for each reserve. + */ + function tryCalcLPTokenUnderlying( + IMultiFlowPumpWellFunction wf, + uint256 lpTokenAmount, + uint256[] memory reserves, + uint256 lpTokenSupply, + bytes memory data + ) internal view returns (uint256[] memory underlyingAmounts) { + try wf.calcLPTokenUnderlying(lpTokenAmount, reserves, lpTokenSupply, data) returns ( + uint256[] memory _underlyingAmounts + ) { + underlyingAmounts = _underlyingAmounts; + for (uint256 i; i < underlyingAmounts.length; ++i) { + if (underlyingAmounts[i] == 0) { + underlyingAmounts[i] = 1; + } + } + } catch { + underlyingAmounts = new uint256[](reserves.length); + for (uint256 i; i < reserves.length; ++i) { + underlyingAmounts[i] = type(uint256).max; + } + } + } } diff --git a/test/TestHelper.sol b/test/TestHelper.sol index 738aaffd..f8fef634 100644 --- a/test/TestHelper.sol +++ b/test/TestHelper.sol @@ -60,6 +60,7 @@ abstract contract TestHelper is Test, WellDeployer { IERC20[] tokens; Call wellFunction; Call[] pumps; + bytes[] pumpData; bytes wellData; // Registry @@ -203,7 +204,7 @@ abstract contract TestHelper is Test, WellDeployer { _pumps = new Call[](n); for (uint256 i; i < n; i++) { _pumps[i].target = address(new MockPump()); - _pumps[i].data = new bytes(i); + _pumps[i].data = new bytes(0); } } @@ -297,6 +298,48 @@ abstract contract TestHelper is Test, WellDeployer { assertApproxEqRelN(a, b, 1, precision); } + function assertApproxLeRelN(uint256 a, uint256 b, uint256 precision, uint256 absoluteError) internal { + console.log("A: %s", a); + console.log("B: %s", b); + console.log(precision); + uint256 numDigitsA = numDigits(a); + uint256 numDigitsB = numDigits(b); + if (numDigitsA != numDigitsB || numDigitsA < precision) { + if (b + absoluteError < type(uint256).max) { + assertLe(a, b + absoluteError); + } + } else { + uint256 denom = 10 ** (numDigits(a) - precision); + uint256 maxB = b / denom; + console.log("Max B", maxB); + console.log("Max B", maxB + absoluteError); + if (maxB + absoluteError < type(uint256).max) { + assertLe(a / denom, maxB + absoluteError); + } + } + } + + function assertApproxGeRelN(uint256 a, uint256 b, uint256 precision, uint256 absoluteError) internal { + console.log("A: %s", a); + console.log("B: %s", b); + console.log(precision); + uint256 numDigitsA = numDigits(a); + uint256 numDigitsB = numDigits(b); + if (numDigitsA != numDigitsB || numDigitsA < precision) { + console.log("Here for some reason"); + if (b > absoluteError) { + assertGe(a, b - absoluteError); + } + } else { + uint256 denom = 10 ** (numDigits(a) - precision); + uint256 minB = b / denom; + console.log("Min B: %s, Abs Err: %s", minB, absoluteError); + if (minB > absoluteError) { + assertGe(a / denom, minB - absoluteError); + } + } + } + function assertApproxEqRelN( uint256 a, uint256 b, diff --git a/test/Well.SwapFrom.t.sol b/test/Well.SwapFrom.t.sol index e3f3e63b..190d3c79 100644 --- a/test/Well.SwapFrom.t.sol +++ b/test/Well.SwapFrom.t.sol @@ -22,7 +22,7 @@ contract WellSwapFromTest is SwapHelper { function testFuzz_getSwapOut_revertIf_insufficientWellBalance(uint256 amountIn, uint256 i) public prank(user) { // Swap token `i` -> all other tokens - vm.assume(i < tokens.length); + i = bound(i, 0, 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. @@ -80,7 +80,7 @@ contract WellSwapFromTest is SwapHelper { /// @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)); + token0AmtIn = bound(token0AmtIn, 0, 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); diff --git a/test/Well.SwapTo.t.sol b/test/Well.SwapTo.t.sol index 59fb5181..eec8f30a 100644 --- a/test/Well.SwapTo.t.sol +++ b/test/Well.SwapTo.t.sol @@ -22,7 +22,7 @@ contract WellSwapToTest is SwapHelper { 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); + i = bound(i, 0, _tokens.length); // Swap token `i` -> all other tokens for (uint256 j; j < _tokens.length; ++j) { diff --git a/test/differential/ConstantProduct.py b/test/differential/ConstantProduct.py new file mode 100644 index 00000000..d6f3454e --- /dev/null +++ b/test/differential/ConstantProduct.py @@ -0,0 +1,56 @@ +import math + +EXP_PRECISION = 1e12 + +class ConstantProduct: + + def __init__(self): + pass + + def calcLPTokenUnderlying( + self, + lpTokenAmount, + reserves, + lpTokenSupply + ): + return [lpTokenAmount * r / lpTokenSupply for r in reserves] + + def calcLpTokenSupply( + self, + reserves + ): + return math.sqrt(reserves[0] * reserves[1] * EXP_PRECISION) + + def calcReserve( + self, + reserves, + j, + lpTokenSupply + ): + return (lpTokenSupply ** 2) / (reserves[0 if j == 1 else 1] * EXP_PRECISION) + + def calcReserveAtRatioSwap( + self, + reserves, + j, + ratios + ): + i = 0 if j == 1 else 1 + return math.sqrt((reserves[i] * reserves[j]) * (ratios[j] / ratios[i])) + + def calcReserveAtRatioLiquidity( + self, + reserves, + j, + ratios + ): + i = 0 if j == 1 else 1 + return reserves[i] * ratios[j] / ratios[i] + + def calcRate( + self, + reserves, + i, + j + ): + return reserves[i] / reserves[j] \ No newline at end of file diff --git a/test/differential/cap_reserves.py b/test/differential/cap_reserves.py new file mode 100644 index 00000000..0688e824 --- /dev/null +++ b/test/differential/cap_reserves.py @@ -0,0 +1,116 @@ +import argparse +from eth_abi import encode +from decimal import * +getcontext().prec = 40 +from ConstantProduct import ConstantProduct + +def printIfV(message, v): + if (v): + print(message) + +def capReserves(wf, xs_last_l, xs_t, capExponent, max_lp_increase, max_lp_decrease, max_rs, verbose=False): + # Put a cap to prevent overflow + if capExponent > 14000: + capExponent = 14000 + + + # xs_rate_t = cap_ratios(wf, xs_last_l, xs_t, capExponent, max_rs, verbose) + xs_rate_t = cap_lp_supply(wf, xs_last_l, xs_t, capExponent, max_lp_increase, max_lp_decrease) + + printIfV("Partial: " + str(xs_rate_t), verbose) + + # return cap_lp_supply(wf, xs_last_l, xs_rate_t, capExponent, max_lp_increase, max_lp_decrease) + return cap_ratios(wf, xs_last_l, xs_rate_t, capExponent, max_rs, verbose) + + +def cap_ratios(wf, xs_last_l, xs_t, capExponent, max_rs, verbose=False): + # Step 1 - Cap Rates + r_01_last_l = wf.calcRate(xs_last_l, 0, 1) + # Put a cap to prevent overflow + if capExponent > 14000: + capExponent = 14000 + printIfV((1 + max_rs[0][1])**capExponent, verbose) + printIfV((1 - max_rs[1][0])**capExponent, verbose) + rs_max_t = [ + [ + 0, + r_01_last_l * (1 + max_rs[0][1])**capExponent, + ], + + [ + 1 / r_01_last_l * (1 + max_rs[1][0])**capExponent, + 0 + ] + ] + printIfV(rs_max_t, verbose) + xs_rate_t = xs_t[:] + if (xs_t[0] / xs_t[1] > rs_max_t[0][1]): + xs_rate_t[0] = wf.calcReserveAtRatioSwap(xs_t, 0, [rs_max_t[0][1], 1]) + xs_rate_t[1] = wf.calcReserveAtRatioSwap(xs_t, 1, [rs_max_t[0][1], 1]) + elif (xs_t[1] / xs_t[0] > rs_max_t[1][0]): + xs_rate_t[0] = wf.calcReserveAtRatioSwap(xs_t, 0, [1, rs_max_t[1][0]]) + xs_rate_t[1] = wf.calcReserveAtRatioSwap(xs_t, 1, [1, rs_max_t[1][0]]) + return xs_rate_t + +def cap_lp_supply(wf, xs_last_l, xs_rate_t, capExponent, max_lp_increase, max_lp_decrease): + # Step 2 - Cap Magnitudes + + k_last_l = wf.calcLpTokenSupply(xs_last_l) + k_rate_t = wf.calcLpTokenSupply(xs_rate_t) + + k_max_t = k_last_l * (1 + max_lp_increase)**capExponent + k_min_t = k_last_l * (1 - max_lp_decrease)**capExponent + + xs_last_t = xs_rate_t + + if (k_rate_t > k_max_t): + xs_last_t = wf.calcLPTokenUnderlying(k_max_t, xs_rate_t, k_rate_t) + elif (k_rate_t < k_min_t): + xs_last_t = wf.calcLPTokenUnderlying(k_min_t, xs_rate_t, k_rate_t) + return xs_last_t + +def main(args): + wf = ConstantProduct() + [reserve0, reserve1] = capReserves( + wf, + [args.last_reserve0, args.last_reserve1], + [args.reserve0, args.reserve1], + args.capExponent, + args.max_lp_increase / 1e18, + args.max_lp_decrease / 1e18, + [[0, args.max_ratio_changes_01 / 1e18], [args.max_ratio_changes_10 / 1e18, 0]] + ) + powu_enc = encode(['uint256', 'uint256'], [int(reserve0), int(reserve1)]) + print("0x" + powu_enc.hex()) + +def test(args): + wf = ConstantProduct() + [reserve0, reserve1] = capReserves( + wf, + [args.last_reserve0, args.last_reserve1], + [args.reserve0, args.reserve1], + args.capExponent, + args.max_lp_increase / 1e18, + args.max_lp_decrease / 1e18, + [[0, args.max_ratio_changes_01 / 1e18], [args.max_ratio_changes_10 / 1e18, 0]], + True + ) + print(int(reserve0), int(reserve1)) + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--reserve0", "-r0", type=int) + parser.add_argument("--reserve1", "-r1", type=int) + parser.add_argument("--last_reserve0", "-l0", type=int) + parser.add_argument("--last_reserve1", "-l1", type=int) + parser.add_argument("--capExponent", "-c", type=int) + parser.add_argument("--max_lp_increase", "-mi", type=int) + parser.add_argument("--max_lp_decrease", "-md", type=int) + parser.add_argument("--max_ratio_changes_01", "-mr01", type=int) + parser.add_argument("--max_ratio_changes_10", "-mr10", type=int) + return parser.parse_args() + +if __name__ == '__main__': + args = parse_args() + main(args) + # test(args) diff --git a/test/differential/powu.py b/test/differential/powu.py index 0d3c74ca..7a1799d2 100644 --- a/test/differential/powu.py +++ b/test/differential/powu.py @@ -1,5 +1,5 @@ import argparse -from eth_abi import encode_single +from eth_abi import encode from decimal import * getcontext().prec = 40 @@ -8,7 +8,7 @@ def powuFraction(num, denom, exp): def main(args): powu = powuFraction(args.numerator, args.denominator, args.exponent) * (2**128) - powu_enc = encode_single('int256', int(powu)) + powu_enc = encode(['int256'], [int(powu)]) print("0x" + powu_enc.hex()) # def test(args): diff --git a/test/functions/ConstantProduct.t.sol b/test/functions/ConstantProduct.t.sol index 6acf99ec..b2635af7 100644 --- a/test/functions/ConstantProduct.t.sol +++ b/test/functions/ConstantProduct.t.sol @@ -19,8 +19,7 @@ contract ConstantProductTest is WellFunctionHelper { /// @dev calcLpTokenSupply: `n` equal reserves should summate with the token supply function testLpTokenSupplySmall(uint256 n) public { - vm.assume(n < 16); - vm.assume(n >= 2); + n = bound(n, 2, 15); uint256[] memory reserves = new uint256[](n); for (uint256 i; i < n; ++i) { reserves[i] = 1; diff --git a/test/functions/ConstantProduct2.t.sol b/test/functions/ConstantProduct2.t.sol index 82d2030d..b4df4429 100644 --- a/test/functions/ConstantProduct2.t.sol +++ b/test/functions/ConstantProduct2.t.sol @@ -164,4 +164,21 @@ contract ConstantProduct2Test is WellFunctionHelper { vm.expectRevert(IWellFunction.InvalidJArgument.selector); _function.calcReserve(reserves, 2, 1e18, _data); } + + function test_calcRate() public { + uint256[] memory reserves = new uint256[](2); + reserves[0] = 100; + reserves[1] = 1; + assertEq(_function.calcRate(reserves, 0, 1, _data), 100e18); + assertEq(_function.calcRate(reserves, 1, 0, _data), 0.01e18); + } + + function test_fuzz_calcRate(uint256[2] memory _reserves) public { + uint256[] memory reserves = new uint256[](2); + reserves[0] = bound(_reserves[0], 1, MAX_RESERVE); + reserves[1] = bound(_reserves[1], 1, MAX_RESERVE); + assertEq(_function.calcRate(reserves, 0, 1, _data), reserves[0] * 1e18 / reserves[1]); + + assertEq(_function.calcRate(reserves, 1, 0, _data), reserves[1] * 1e18 / reserves[0]); + } } diff --git a/test/functions/WellFunctionHelper.sol b/test/functions/WellFunctionHelper.sol index 80602b61..d79c0341 100644 --- a/test/functions/WellFunctionHelper.sol +++ b/test/functions/WellFunctionHelper.sol @@ -2,19 +2,18 @@ pragma solidity ^0.8.20; import {TestHelper, console, stdError} from "test/TestHelper.sol"; -import {IWellFunction} from "src/interfaces/IWellFunction.sol"; +import {IMultiFlowPumpWellFunction} from "src/interfaces/IMultiFlowPumpWellFunction.sol"; /// @dev Provides a base test suite for all Well functions. abstract contract WellFunctionHelper is TestHelper { - IWellFunction _function; + IMultiFlowPumpWellFunction _function; bytes _data; /// @dev calcLpTokenSupply: 0 reserves = 0 supply /// Some Well Functions will choose to support > 2 tokens. /// Additional tokens passed in `reserves` should be ignored. function test_calcLpTokenSupply_empty(uint256 n) public { - vm.assume(n < 16); - vm.assume(n >= 2); + n = bound(n, 2, 15); uint256[] memory reserves = new uint256[](n); assertEq(_function.calcLpTokenSupply(reserves, _data), 0); } diff --git a/test/integration/IntegrationTestHelper.sol b/test/integration/IntegrationTestHelper.sol index ea792ef3..b4731187 100644 --- a/test/integration/IntegrationTestHelper.sol +++ b/test/integration/IntegrationTestHelper.sol @@ -17,10 +17,7 @@ abstract contract IntegrationTestHelper is TestHelper { function setupWell(IERC20[] memory _tokens, Well _well) internal returns (Well) { Call[] memory _pumps = new Call[](1); - _pumps[0] = Call( - address(new MultiFlowPump(from18(0.5e18), from18(0.333333333333333333e18), 12, from18(0.9e18))), - new bytes(0) - ); + _pumps[0] = Call(address(new MultiFlowPump()), new bytes(0)); return setupWell(_tokens, Call(address(new ConstantProduct2()), new bytes(0)), _pumps, _well); } diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index cea47b1b..4984d9bb 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -19,12 +19,7 @@ contract Invariants is LiquidityHelper { function setUp() public { // setup the pump - IPump pump = new MultiFlowPump( - from18(0.5e18), - from18(0.333333333333333333e18), - 12, - from18(0.9e18) - ); + IPump pump = new MultiFlowPump(); Call[] memory pumps = new Call[](1); pumps[0] = Call({target: address(pump), data: new bytes(0)}); diff --git a/test/libraries/LibBytes.t.sol b/test/libraries/LibBytes.t.sol index 82da8093..9df16664 100644 --- a/test/libraries/LibBytes.t.sol +++ b/test/libraries/LibBytes.t.sol @@ -11,7 +11,7 @@ contract LibBytesTest is TestHelper { /// @dev Store fuzzed reserves, re-read and compare. function testFuzz_storeAndRead(uint256 n, uint128[8] memory _reserves) public { - vm.assume(n <= NUM_RESERVES_MAX); + n = bound(n, 0, NUM_RESERVES_MAX); // Use the first `n` reserves. Cast uint128 reserves -> uint256 uint256[] memory reserves = new uint256[](n); @@ -59,9 +59,8 @@ contract LibBytesTest is TestHelper { /// @dev Fuzz test different sizes of reserves array and different positions /// for overflow. reserves besides `reserves[j]` can be non-zero. function testFuzz_storeUint128_overflow(uint256 n, uint256 tooLargeIndex, uint128[8] memory _reserves) public { - vm.assume(n <= NUM_RESERVES_MAX); - vm.assume(n > 0); - vm.assume(tooLargeIndex < n); + tooLargeIndex = bound(tooLargeIndex, 0, NUM_RESERVES_MAX - 1); + n = bound(n, tooLargeIndex + 1, NUM_RESERVES_MAX); // Use the first `n` reserves. Cast uint128 reserves -> uint256 uint256[] memory reserves = new uint256[](n); diff --git a/test/libraries/LibBytes16.t.sol b/test/libraries/LibBytes16.t.sol index dc1142e0..9cc5a337 100644 --- a/test/libraries/LibBytes16.t.sol +++ b/test/libraries/LibBytes16.t.sol @@ -14,7 +14,7 @@ contract LibBytes16Test is TestHelper { /// @dev Store fuzzed reserves, re-read and compare. function testFuzz_storeAndReadBytes16(uint256 n, bytes16[8] memory _reserves) public { - vm.assume(n <= NUM_RESERVES_MAX); + n = bound(n, 0, NUM_RESERVES_MAX); // Use the first `n` reserves. Cast uint128 reserves -> uint256 bytes16[] memory reserves = new bytes16[](n); diff --git a/test/libraries/LibLastReserveBytes.t.sol b/test/libraries/LibLastReserveBytes.t.sol index e2f2787a..8958028f 100644 --- a/test/libraries/LibLastReserveBytes.t.sol +++ b/test/libraries/LibLastReserveBytes.t.sol @@ -15,25 +15,25 @@ contract LibLastReserveBytesTest is TestHelper { function testEmaFuzz_storeAndRead( uint8 n, uint40 lastTimestamp, - bytes13[NUM_RESERVES_MAX] memory _reserves + uint256[NUM_RESERVES_MAX] memory _reserves ) public { vm.assume(n <= NUM_RESERVES_MAX); // Use the first `n` reserves. Cast uint104 reserves -> uint256 - bytes16[] memory reserves = new bytes16[](n); + uint256[] memory reserves = new uint256[](n); for (uint256 i; i < n; i++) { - reserves[i] = bytes16(_reserves[i]) << 24; + reserves[i] = _reserves[i]; } RESERVES_STORAGE_SLOT.storeLastReserves(lastTimestamp, reserves); // Re-read reserves and compare - (uint8 _n, uint40 _lastTimestamp, bytes16[] memory reserves2) = RESERVES_STORAGE_SLOT.readLastReserves(); + (uint8 _n, uint40 _lastTimestamp, uint256[] memory reserves2) = RESERVES_STORAGE_SLOT.readLastReserves(); uint8 __n = RESERVES_STORAGE_SLOT.readNumberOfReserves(); assertEq(__n, n, "ByteStorage: n mismatch"); assertEq(_n, n, "ByteStorage: n mismatch"); assertEq(_lastTimestamp, lastTimestamp, "ByteStorage: lastTimestamp mismatch"); for (uint256 i; i < reserves2.length; i++) { - assertEq(reserves2[i], reserves[i], "ByteStorage: reserves mismatch"); + assertApproxEqRelN(reserves2[i], reserves[i], 1); } } } diff --git a/test/libraries/TestABDK.t.sol b/test/libraries/TestABDK.t.sol index db16342c..4fd6b602 100644 --- a/test/libraries/TestABDK.t.sol +++ b/test/libraries/TestABDK.t.sol @@ -17,7 +17,7 @@ contract ABDKTest is TestHelper { * @dev no hysteresis: 2^(log2(a)) == a +/- 1 (due to library rounding) */ function testFuzz_log2Pow2(uint256 a) public { - vm.assume(a > 0); + a = bound(a, 1, type(uint256).max); uint256 b = (a.fromUInt().log_2()).pow_2().toUInt(); if (a <= 1e18) { assertApproxEqAbs(a, b, 1); @@ -68,12 +68,12 @@ contract ABDKTest is TestHelper { } function testFuzz_FromUIntToLog2(uint256 x) public { - vm.assume(x > 0); // log2(0) is undefined. + x = bound(x, 1, type(uint256).max); assertEq(ABDKMathQuad.fromUInt(x).log_2(), ABDKMathQuad.fromUIntToLog2(x)); } function testFuzz_pow_2ToUInt(uint256 x) public { - vm.assume(x < 256); // the max value of an uint256 is 2^256 - 1. + x = bound(x, 0, 255); // test the pow_2ToUInt function bytes16 _x = x.fromUInt(); diff --git a/test/pumps/Pump.CapReserve.t.sol b/test/pumps/Pump.CapReserve.t.sol deleted file mode 100644 index 1911dfde..00000000 --- a/test/pumps/Pump.CapReserve.t.sol +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {TestHelper, console} from "test/TestHelper.sol"; -import {MultiFlowPump, ABDKMathQuad} from "src/pumps/MultiFlowPump.sol"; -import {simCapReserve50Percent, from18, to18} from "test/pumps/PumpHelpers.sol"; -import {log2, powu, UD60x18, wrap, unwrap} from "prb/math/UD60x18.sol"; -import {exp2, log2, powu, UD60x18, wrap, unwrap, uUNIT} from "prb/math/UD60x18.sol"; - -contract CapBalanceTest is TestHelper, MultiFlowPump { - using ABDKMathQuad for bytes16; - - constructor() - MultiFlowPump( - from18(0.5e18), // cap reserves if changed +/- 50% per block - from18(0.5e18), // cap reserves if changed +/- 50% per block - 12, // EVM block time - from18(0.9994445987e18) // geometric EMA constant - ) - {} - - ////////// Cap: Increase - - function testFuzz_capReserve_capped0BlockIncrease(uint256 last, uint256 curr) public { - // ensure that curr is greater than 2*last to simulate >= 100% increase - last = bound(last, 0, type(uint256).max / 2); - curr = bound(curr, last * 2, type(uint256).max); - - uint256 balance = ABDKMathQuad.toUInt( - _capReserve(ABDKMathQuad.fromUIntToLog2(last), ABDKMathQuad.fromUIntToLog2(curr), ABDKMathQuad.fromUInt(0)) - .pow_2() - ); - - if (last < 1e24) { - assertApproxEqAbs(balance, last, 1); - } else { - assertApproxEqRel(balance, last, 1); - } - } - - function testFuzz_capReserve_xBlocks(uint256 lastBalance, uint256 balance, uint256 blocks) public { - // ensure that curr is greater than 2*last to simulate >= 100% increase - // TODO: Potentially relax assumption. Going too high causes arithmetic overflow. - lastBalance = bound(lastBalance, 100, type(uint128).max); - balance = bound(balance, 100, type(uint128).max); - blocks = bound(blocks, 1, 2 ** 12); - - // Add precision for the capReserve function - uint256 expectedCappedBalance = simCapReserve50Percent(lastBalance, balance, blocks); - - console.log("Expected capped balance", expectedCappedBalance); - - uint256 cappedBalance = _capReserve( - ABDKMathQuad.fromUIntToLog2(lastBalance), - ABDKMathQuad.fromUIntToLog2(balance), - ABDKMathQuad.fromUInt(blocks) - ).pow_2ToUInt(); - - if (cappedBalance < 1e22) { - assertApproxEqAbs(cappedBalance, expectedCappedBalance, 1); - } else { - assertApproxEqRelN(cappedBalance, expectedCappedBalance, 1, 22); - } - } - - function test_capReserve_capped1BlockIncrease() public { - uint256 balance = ABDKMathQuad.toUInt( - // 1e16 -> 200e16 over 1 block is more than +/- 50% - // First block: 1 * (1 + 50%) = 1.5 [e16] - _capReserve( - ABDKMathQuad.fromUInt(1e16).log_2(), ABDKMathQuad.fromUInt(200e16).log_2(), ABDKMathQuad.fromUInt(1) - ).pow_2() - ); - assertApproxEqAbs(balance, 1.5e16, 1); - } - - function test_capReserve_uncapped2BlockIncrease() public { - uint256 balance = ABDKMathQuad.toUInt( - // 1e16 -> 1.2e16 over 2 blocks is within +/- 50% - _capReserve( - ABDKMathQuad.fromUInt(1e16).log_2(), ABDKMathQuad.fromUInt(1.2e16).log_2(), ABDKMathQuad.fromUInt(2) - ).pow_2() - ); - assertApproxEqAbs(balance, 1.2e16, 1); - } - - function test_capReserve_capped2BlockIncrease() public { - uint256 balance = ABDKMathQuad.toUInt( - // 1e16 -> 200e16 over 2 blocks is more than +/- 50% - // First block: 1 * (1 + 50%) = 1.5 [e16] - // Second block: 1.5 * (1 + 50%) = 2.25 [e16] - _capReserve( - ABDKMathQuad.fromUInt(1e16).log_2(), ABDKMathQuad.fromUInt(200e16).log_2(), ABDKMathQuad.fromUInt(2) - ).pow_2() - ); - assertApproxEqAbs(balance, 2.25e16, 1); - } - - ////////// Cap: Decrease - - function test_capReserve_capped1BlockDecrease() public { - uint256 balance = ABDKMathQuad.toUInt( - // 1e16 -> 0.000002e16 over 1 block is more than +/- 50% - _capReserve( - ABDKMathQuad.fromUInt(1e16).log_2(), ABDKMathQuad.fromUInt(2e10).log_2(), ABDKMathQuad.fromUInt(1) - ).pow_2() - ); - assertApproxEqAbs(balance, 0.5e16, 1); - } - - function test_capReserve_uncapped1BlockDecrease() public { - uint256 balance = ABDKMathQuad.toUInt( - // 1e16 -> 0.75e16 over 1 block is within +/- 50% - _capReserve( - ABDKMathQuad.fromUInt(1e16).log_2(), ABDKMathQuad.fromUInt(0.75e16).log_2(), ABDKMathQuad.fromUInt(1) - ).pow_2() - ); - assertApproxEqAbs(balance, 0.75e16, 1); - } - - function test_capReserve_capped2BlockDecrease() public { - uint256 balance = ABDKMathQuad.toUInt( - // 1e16 -> 0.000002e16 over 2 blocks is more than +/- 50% - // First block: 1 * (1 - 50%) = 0.5 [e16] - // Second block: 0.5 * (1 - 50%) = 0.25 [e16] - _capReserve( - ABDKMathQuad.fromUInt(1e16).log_2(), ABDKMathQuad.fromUInt(2e10).log_2(), ABDKMathQuad.fromUInt(2) - ).pow_2() - ); - assertApproxEqAbs(balance, 0.25e16, 1); - } - - ////////// Cap: Simulate - - struct CapReservePoint { - uint256 j; - uint256 prev; - uint256 curr; - uint256 capped; - } - - function testSim_capReserve_increase() public { - _simulate(1e16, 200e16, 16, "capReserve_increase"); - } - - function testSim_capReserve_decrease() public { - _simulate(1e16, 2e10, 32, "capReserve_decrease"); - } - - function _simulate(uint256 prev, uint256 curr, uint256 n, string memory name) internal { - CapReservePoint[] memory pts = new CapReservePoint[](n); - uint256 capped = prev; - for (uint256 j = 1; j <= n; ++j) { - capped = ABDKMathQuad.toUInt( - _capReserve( - ABDKMathQuad.fromUInt(prev).log_2(), ABDKMathQuad.fromUInt(curr).log_2(), ABDKMathQuad.fromUInt(j) - ).pow_2() - ); - pts[j - 1] = CapReservePoint(j, prev, curr, capped); - } - _save(name, abi.encode(pts)); - } - - function _save(string memory f, bytes memory s) internal { - string[] memory inputs = new string[](6); - inputs[0] = "python3"; - inputs[1] = "test/pumps/simulate.py"; - inputs[2] = "--data"; - inputs[3] = _bytesToHexString(s); - inputs[4] = "--name"; - inputs[5] = f; - vm.ffi(inputs); - } - - function _bytesToHexString(bytes memory buffer) public pure returns (string memory) { - // Fixed buffer size for hexadecimal convertion - bytes memory converted = new bytes(buffer.length * 2); - bytes memory _base = "0123456789abcdef"; - for (uint256 i; i < buffer.length; i++) { - converted[i * 2] = _base[uint8(buffer[i]) / _base.length]; - converted[i * 2 + 1] = _base[uint8(buffer[i]) % _base.length]; - } - return string(abi.encodePacked("0x", converted)); - } -} diff --git a/test/pumps/Pump.CapReserves.t.sol b/test/pumps/Pump.CapReserves.t.sol new file mode 100644 index 00000000..d1586153 --- /dev/null +++ b/test/pumps/Pump.CapReserves.t.sol @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {TestHelper, console} from "test/TestHelper.sol"; +import {Call, IERC20} from "src/interfaces/IWell.sol"; +import {ConstantProduct2} from "src/functions/ConstantProduct2.sol"; +import {MultiFlowPump, IWell, IMultiFlowPumpWellFunction, SafeCast, ABDKMathQuad} from "src/pumps/MultiFlowPump.sol"; +import {simCapReserve50Percent, from18, to18} from "test/pumps/PumpHelpers.sol"; +import {log2, powu, UD60x18, wrap, unwrap} from "prb/math/UD60x18.sol"; +import {exp2, log2, powu, UD60x18, wrap, unwrap, uUNIT} from "prb/math/UD60x18.sol"; +import {MockStaticWell} from "mocks/wells/MockStaticWell.sol"; +import {ReentrancyGuardUpgradeable} from "ozu/security/ReentrancyGuardUpgradeable.sol"; +import {Math} from "oz/utils/math/Math.sol"; +import "oz/utils/Strings.sol"; + +contract CapBalanceTest is TestHelper, MultiFlowPump { + using ABDKMathQuad for bytes16; + using SafeCast for int256; + using Math for uint256; + using Strings for uint256; + + uint256[] lastReserves; + uint256[] reserves; + bytes16[][] maxRateChanges; + bytes16 maxLpSupplyIncrease; + bytes16 maxLpSupplyDecrease; + // uint256 MAX_RESERVE = 1e30; + uint256 MAX_RESERVE = 1e24; + CapReservesParameters crp; + + address _well; + + ConstantProduct2 public wf; + + constructor() MultiFlowPump() { + lastReserves = new uint256[](2); + reserves = new uint256[](2); + + maxLpSupplyIncrease = from18(0.05e18); + maxLpSupplyDecrease = from18(0.04761904762e18); + + maxRateChanges = new bytes16[][](2); + maxRateChanges[0] = new bytes16[](2); + maxRateChanges[1] = new bytes16[](2); + maxRateChanges[0][1] = from18(0.05e18); + maxRateChanges[1][0] = from18(0.05e18); + + crp = MultiFlowPump.CapReservesParameters(maxRateChanges, maxLpSupplyIncrease, maxLpSupplyDecrease); + + wf = new ConstantProduct2(); + + _well = address( + new MockStaticWell( + deployMockTokens(2), Call(address(wf), new bytes(0)), deployPumps(1), address(0), new bytes(0) + ) + ); + } + + function test_capReserve_belowCap() public { + lastReserves[0] = 100; + lastReserves[1] = 100; + + reserves[0] = 101; + reserves[1] = 102; + + uint256[] memory cappedReserves = _capReserves( + address(_well), + lastReserves, + reserves, + 1, // capExponent + CapReservesParameters(maxRateChanges, maxLpSupplyIncrease, maxLpSupplyDecrease) + ); + + assertEq(cappedReserves[0], 101); + assertEq(cappedReserves[1], 102); + } + + function test_capReserve_aboveRatioCap() public { + lastReserves[0] = 1000; + lastReserves[1] = 1000; + + reserves[0] = 980; + reserves[1] = 1031; + + uint256[] memory cappedReserves = _capReserves( + address(_well), + lastReserves, + reserves, + 1, // capExponent + crp + ); + + assertEq(cappedReserves[0], 980); + assertEq(cappedReserves[1], 1029); + } + + function test_capReserve_belowRatioCap() public { + lastReserves[0] = 1000; + lastReserves[1] = 1000; + + reserves[0] = 979; + reserves[1] = 1029; + + uint256[] memory cappedReserves = _capReserves( + address(_well), + lastReserves, + reserves, + 1, // capExponent + crp + ); + + assertEq(cappedReserves[0], 979); + assertEq(cappedReserves[1], 1028); + } + + function testFuzz_capReserve_oneBlock(uint256[2] memory _lastReserves, uint256[2] memory _reserves) public { + lastReserves = new uint256[](2); + lastReserves[0] = bound(_lastReserves[0], 1e6, MAX_RESERVE); + lastReserves[1] = bound( + _lastReserves[1], + Math.max(1e6, lastReserves[0] / MultiFlowPump.CAP_PRECISION * 10), + Math.min(MAX_RESERVE, lastReserves[0] * MultiFlowPump.CAP_PRECISION / 10) + ); + + reserves = new uint256[](2); + reserves[0] = bound(_reserves[0], 1e6, MAX_RESERVE); + reserves[1] = bound( + _reserves[1], + Math.max(1e6, reserves[0] / MultiFlowPump.CAP_PRECISION), + Math.min(MAX_RESERVE, reserves[0] * MultiFlowPump.CAP_PRECISION) + ); + + uint256[] memory cappedReserves = _capReserves( + address(_well), + lastReserves, + reserves, + 1, // capExponent + crp + ); + + uint256 ratioDigits = getRatioDigits(address(_well), lastReserves, reserves, 1, crp); + uint256 precision = numDigits(cappedReserves[0]).min(numDigits(cappedReserves[1])).min(numDigits(reserves[0])) + .min(numDigits(reserves[1])).min(ratioDigits); + + if (precision >= 2) precision = precision - 2; + else precision = 1; + console.log("Digit precision: %s", precision); + uint256 absolutePrecision = 1; + + uint256 lpTokenSupplyBefore = wf.calcLpTokenSupply(lastReserves, new bytes(0)); + uint256 maxLpTokenSupply = lpTokenSupplyBefore * (1e18 + to18(maxLpSupplyIncrease)) / 1e18; + uint256 minLpTokenSupply = lpTokenSupplyBefore * (1e18 - to18(maxLpSupplyDecrease)) / 1e18; + uint256 lpTokenSupplyCapped = wf.calcLpTokenSupply(cappedReserves, new bytes(0)); + console.log("LP Token Supply Before: %s", lpTokenSupplyBefore); + console.log("LP Token Supply Max: %s", maxLpTokenSupply); + console.log("LP Token Supply Capped: %s", lpTokenSupplyCapped); + console.log("LP Token Supply Min: %s", minLpTokenSupply); + + precision = precision.min(numDigits(lpTokenSupplyCapped.sqrt())); + console.log("LP Token Supply Digits: %s", precision); + + assertApproxGeRelN(lpTokenSupplyCapped, minLpTokenSupply, precision, absolutePrecision); + assertApproxLeRelN(lpTokenSupplyCapped, maxLpTokenSupply, precision, absolutePrecision); + + assertNotEq(cappedReserves[0], 0); + assertNotEq(cappedReserves[1], 0); + + if (cappedReserves[0] == 1) return; + if (cappedReserves[1] == 1) return; + + (uint256 i, uint256 j) = lastReserves[0] > lastReserves[1] ? (0, 1) : (1, 0); + + uint256 rIJMax = lastReserves[i] * (1e18 + to18(maxRateChanges[i][j])) / lastReserves[j]; + uint256 rIJMin = lastReserves[i] * 1e18 / (1 + to18(maxRateChanges[i][j])) / lastReserves[j]; + uint256 rIJCapped = cappedReserves[i] * 1e18 / cappedReserves[j]; + console.log("Max R: %s", rIJMax); + console.log("Min R: %s", rIJMin); + console.log("Output R: %s", rIJCapped); + console.log("Checking Max!"); + assertApproxLeRelN(rIJCapped, rIJMax, precision, absolutePrecision); + console.log("Checking Min!"); + assertApproxGeRelN(rIJCapped, rIJMin, precision, absolutePrecision); + } + + function testFuzzInstance_capReserve() public { + testFuzz_capReserve_xBlock( + [uint256(668_374_840_427_059_908_583_306_633_348), 999_999_999_999_999_993_316_251_595_735], + [uint256(935_068_122_923_189_688_180_993_425_409), 944_816_668_711_320_003_773_400_140_733], + 3 + ); + } + + function testFuzz_capReserve_xBlock( + uint256[2] memory _lastReserves, + uint256[2] memory _reserves, + uint256 capExponent + ) public { + lastReserves = new uint256[](2); + lastReserves[0] = bound(_lastReserves[0], 1e6, MAX_RESERVE); + lastReserves[1] = bound( + _lastReserves[1], + Math.max(1e6, lastReserves[0] / MultiFlowPump.CAP_PRECISION * 10), + Math.min(MAX_RESERVE, lastReserves[0] * MultiFlowPump.CAP_PRECISION / 10) + ); + + reserves = new uint256[](2); + reserves[0] = bound(_reserves[0], 1e6, MAX_RESERVE); + reserves[1] = bound( + _reserves[1], + Math.max(1e6, reserves[0] / MultiFlowPump.CAP_PRECISION), + Math.min(MAX_RESERVE, reserves[0] * MultiFlowPump.CAP_PRECISION) + ); + + // TODO: Increase bound!!! + capExponent = bound(capExponent, 1, 10_000); + + uint256[] memory cappedReserves = _capReserves(address(_well), lastReserves, reserves, capExponent, crp); + + assertNotEq(cappedReserves[0], 0); + assertNotEq(cappedReserves[1], 0); + + if (cappedReserves[0] == 1) return; + if (cappedReserves[1] == 1) return; + + string[] memory inputs = new string[](20); + inputs[0] = "python"; + inputs[1] = "test/differential/cap_reserves.py"; + (inputs[2], inputs[3]) = ("-r0", reserves[0].toString()); + (inputs[4], inputs[5]) = ("-r1", reserves[1].toString()); + (inputs[6], inputs[7]) = ("-l0", lastReserves[0].toString()); + (inputs[8], inputs[9]) = ("-l1", lastReserves[1].toString()); + (inputs[10], inputs[11]) = ("-c", capExponent.toString()); + (inputs[12], inputs[13]) = ("-mi", uint256(0.05e18).toString()); + (inputs[14], inputs[15]) = ("-md", uint256(0.04761904762e18).toString()); + (inputs[16], inputs[17]) = ("-mr01", uint256(0.05e18).toString()); + (inputs[18], inputs[19]) = ("-mr10", uint256(0.05e18).toString()); + bytes memory result = vm.ffi(inputs); + uint256 lpTokenSupplyCapped = wf.calcLpTokenSupply(cappedReserves, new bytes(0)); + uint256 ratioDigits = getRatioDigits(address(_well), lastReserves, reserves, capExponent, crp); + + console.log("LP Token Supply Capped: %s", lpTokenSupplyCapped); + uint256 precision = numDigits(cappedReserves[0]).min(numDigits(cappedReserves[1])).min(numDigits(reserves[0])) + .min(numDigits(reserves[1])).min(numDigits(lpTokenSupplyCapped.sqrt())); + precision = precision.min(ratioDigits); + if (precision >= 1) precision = precision - 1; + console.log("Digit precision: %s", precision); + + (uint256 expectedCappedReserve0, uint256 expectedCappedReserve1) = abi.decode(result, (uint256, uint256)); + console.log("R0: %s, R1: %s", expectedCappedReserve0, expectedCappedReserve1); + + assertApproxEqRelN(cappedReserves[0], expectedCappedReserve0, 1, precision); + assertApproxEqRelN(cappedReserves[1], expectedCappedReserve1, 1, precision); + } + + function getRatioDigits( + address well, + uint256[] memory _lastReserves, + uint256[] memory _reserves, + uint256 capExponent, + CapReservesParameters memory _crp + ) private view returns (uint256 ratioDigits) { + Call memory _wf = IWell(well).wellFunction(); + IMultiFlowPumpWellFunction mfpWf = IMultiFlowPumpWellFunction(_wf.target); + + (uint256 i, uint256 j) = _lastReserves[0] > _lastReserves[1] ? (0, 1) : (1, 0); + uint256 rLast = mfpWf.calcRate(_lastReserves, i, j, _wf.data); + uint256 r = mfpWf.calcRate(_reserves, i, j, _wf.data); + if (r < rLast) { + ratioDigits = rLast.mulDiv( + ABDKMathQuad.ONE.div(ABDKMathQuad.ONE.add(_crp.maxRateChanges[j][i])).powu(capExponent).to128x128() + .toUint256(), + CAP_PRECISION2 + ); + ratioDigits = numDigits(ratioDigits); + } else { + ratioDigits = type(uint256).max; + } + } +} diff --git a/test/pumps/Pump.Fuzz.t.sol b/test/pumps/Pump.Fuzz.t.sol index 2ac502d0..47df7aad 100644 --- a/test/pumps/Pump.Fuzz.t.sol +++ b/test/pumps/Pump.Fuzz.t.sol @@ -8,29 +8,33 @@ import {console, TestHelper} from "test/TestHelper.sol"; import {ABDKMathQuad, MultiFlowPump} from "src/pumps/MultiFlowPump.sol"; import {MockReserveWell} from "mocks/wells/MockReserveWell.sol"; -import {from18, to18} from "test/pumps/PumpHelpers.sol"; +import {mockPumpData, from18, to18} from "test/pumps/PumpHelpers.sol"; import {log2, powu, UD60x18, wrap, unwrap} from "prb/math/UD60x18.sol"; import {exp2, log2, powu, UD60x18, wrap, unwrap, uUNIT} from "prb/math/UD60x18.sol"; +import {ConstantProduct2} from "src/functions/ConstantProduct2.sol"; + +import {Math} from "oz/utils/math/Math.sol"; contract PumpFuzzTest is TestHelper, MultiFlowPump { using ABDKMathQuad for bytes16; using ABDKMathQuad for uint256; + using Math for uint256; + uint256 constant capInterval = 12; MultiFlowPump pump; + bytes data; MockReserveWell mWell; uint256[] b = new uint256[](2); - constructor() MultiFlowPump(from18(0.5e18), from18(0.333333333333333333e18), 12, from18(0.9e18)) {} + constructor() MultiFlowPump() {} function setUp() public { mWell = new MockReserveWell(); initUser(); - pump = new MultiFlowPump( - from18(0.5e18), - from18(0.333333333333333333e18), - 12, - from18(0.9e18) - ); + pump = new MultiFlowPump(); + data = mockPumpData(); + wellFunction.target = address(new ConstantProduct2()); + mWell.setWellFunction(wellFunction); } /** @@ -47,10 +51,12 @@ contract PumpFuzzTest is TestHelper, MultiFlowPump { uint8 n, uint40 timeIncrease ) public prank(user) { - n = uint8(bound(n, 1, 8)); + // n is bound to 2 in the current iteration of the well. + // n = uint8(bound(n, 1, 8)); + n = 2; for (uint256 i; i < n; i++) { - initReserves[i] = bound(initReserves[i], 1e6, type(uint128).max); - reserves[i] = bound(reserves[i], 1e6, type(uint128).max); + initReserves[i] = bound(initReserves[i], 1e6, 1e32); + reserves[i] = bound(reserves[i], 1e6, 1e32); } vm.assume(block.timestamp + timeIncrease <= type(uint40).max); @@ -59,49 +65,51 @@ contract PumpFuzzTest is TestHelper, MultiFlowPump { for (uint256 i; i < n; i++) { updateReserves[i] = initReserves[i]; } - mWell.update(address(pump), updateReserves, new bytes(0)); + mWell.update(address(pump), updateReserves, data); for (uint256 i; i < n; i++) { updateReserves[i] = reserves[i]; } - mWell.update(address(pump), updateReserves, new bytes(0)); + mWell.update(address(pump), updateReserves, data); // Read a snapshot from the Pump - bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), new bytes(0)); + bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), data); + + uint256[] memory expectedCappedReserves = pump.readLastCappedReserves(address(mWell), data); // Fast-forward time and update the Pump with new reserves. increaseTime(timeIncrease); - uint256[] memory cappedReserves = pump.readCappedReserves(address(mWell)); + uint256[] memory cappedReserves = pump.readCappedReserves(address(mWell), data); + + mWell.update(address(pump), updateReserves, data); - mWell.update(address(pump), updateReserves, new bytes(0)); + uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell), data); - uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell)); + uint256[] memory _reserves = new uint256[](n); for (uint256 i; i < n; ++i) { - uint256 capReserve; - if (timeIncrease > 0) { - capReserve = _capReserve( - initReserves[i].fromUIntToLog2(), - updateReserves[i].fromUIntToLog2(), - ((timeIncrease - 1) / CAP_INTERVAL + 1).fromUInt() - ).pow_2ToUInt(); - } else { - capReserve = initReserves[i]; - } + _reserves[i] = reserves[i]; + } + (,, CapReservesParameters memory crp) = abi.decode(data, (uint256, uint256, CapReservesParameters)); + if (timeIncrease > 0) { + uint256 capExponent = (timeIncrease - 1) / capInterval + 1; + expectedCappedReserves = _capReserves(address(mWell), expectedCappedReserves, _reserves, capExponent, crp); + } + + for (uint256 i; i < n; ++i) { if (lastReserves[i] > 1e24) { - assertApproxEqRelN(capReserve, lastReserves[i], 1, 24); - assertApproxEqRelN(capReserve, cappedReserves[i], 1, 24); + assertApproxEqRelN(expectedCappedReserves[i], lastReserves[i], 1, 24); + assertApproxEqRelN(expectedCappedReserves[i], cappedReserves[i], 1, 24); } else { - assertApproxEqAbs(capReserve, lastReserves[i], 1); - assertApproxEqAbs(capReserve, cappedReserves[i], 1); + assertApproxEqAbs(expectedCappedReserves[i], lastReserves[i], 1); + assertApproxEqAbs(expectedCappedReserves[i], cappedReserves[i], 1); } } // readTwaReserves reverts if no time has passed. if (timeIncrease > 0) { - (uint256[] memory twaReserves,) = pump.readTwaReserves( - address(mWell), startCumulativeReserves, block.timestamp - timeIncrease, new bytes(0) - ); + (uint256[] memory twaReserves,) = + pump.readTwaReserves(address(mWell), startCumulativeReserves, block.timestamp - timeIncrease, data); for (uint256 i; i < n; ++i) { console.log("TWA RESERVES", i, twaReserves[i]); if (lastReserves[i] > 1e24) { diff --git a/test/pumps/Pump.Helpers.t.sol b/test/pumps/Pump.Helpers.t.sol index e3123ffb..3f51ce22 100644 --- a/test/pumps/Pump.Helpers.t.sol +++ b/test/pumps/Pump.Helpers.t.sol @@ -10,14 +10,7 @@ contract PumpHelpersTest is TestHelper, MultiFlowPump { uint256[5] testCasesInput = [1, 2, 3, 4, 5]; uint256[5] testCasesOutput = [32, 32, 64, 64, 96]; - constructor() - MultiFlowPump( - from18(0.5e18), // cap reserves if changed +/- 50% per block - from18(0.5e18), // cap reserves if changed +/- 50% per block - 12, // EVM block time - from18(0.9994445987e18) // geometric EMA constant - ) - {} + constructor() MultiFlowPump() {} function test_getSlotForAddress() public { address addr = address(0xa755A670Aaf1FeCeF2bea56115E65e03F7722A79); diff --git a/test/pumps/Pump.Longevity.t.sol b/test/pumps/Pump.Longevity.t.sol index 08c24234..1db2beb2 100644 --- a/test/pumps/Pump.Longevity.t.sol +++ b/test/pumps/Pump.Longevity.t.sol @@ -7,6 +7,8 @@ pragma solidity ^0.8.20; import {console, TestHelper} from "test/TestHelper.sol"; import {ABDKMathQuad, MultiFlowPump} from "src/pumps/MultiFlowPump.sol"; import {MockReserveWell} from "mocks/wells/MockReserveWell.sol"; +import {mockPumpData} from "test/pumps/PumpHelpers.sol"; +import {ConstantProduct2} from "src/functions/ConstantProduct2.sol"; import {generateRandomUpdate, from18, to18} from "test/pumps/PumpHelpers.sol"; import {log2, powu, UD60x18, wrap, unwrap} from "prb/math/UD60x18.sol"; @@ -18,6 +20,7 @@ contract PumpLongevityTest is TestHelper { MultiFlowPump pump; MockReserveWell mWell; + bytes data; uint256[] b = new uint256[](2); constructor() {} @@ -25,12 +28,10 @@ contract PumpLongevityTest is TestHelper { function setUp() public { mWell = new MockReserveWell(); initUser(); - pump = new MultiFlowPump( - from18(0.5e18), - from18(0.333333333333333333e18), - 12, - from18(0.9e18) - ); + pump = new MultiFlowPump(); + data = mockPumpData(); + wellFunction.target = address(new ConstantProduct2()); + mWell.setWellFunction(wellFunction); } function testIterate() public prank(user) { @@ -39,14 +40,17 @@ contract PumpLongevityTest is TestHelper { uint256[] memory balances; uint40 timeStep; uint256 timestamp = block.timestamp; - for (uint256 i; i < 30_000; ++i) { + for (uint256 i; i < 4000; ++i) { + if (i % 1000 == 0) { + console.log(i); + } (balances, timeStep, seed) = generateRandomUpdate(n, seed); // console.log("Time Step: ", timeStep); // for (uint256 j; j < n; ++j) { // console.log("Balance", j, balances[j]); // } increaseTime(timeStep); - mWell.update(address(pump), balances, new bytes(0)); + mWell.update(address(pump), balances, data); } // uint256[] memory lastReserves = pump.readLastReserves(address(mWell)); diff --git a/test/pumps/Pump.NotInitialized.t.sol b/test/pumps/Pump.NotInitialized.t.sol index 0191d275..6b6ff6c2 100644 --- a/test/pumps/Pump.NotInitialized.t.sol +++ b/test/pumps/Pump.NotInitialized.t.sol @@ -6,60 +6,58 @@ pragma solidity ^0.8.20; import {console, TestHelper} from "test/TestHelper.sol"; import {MultiFlowPump} from "src/pumps/MultiFlowPump.sol"; +import {mockPumpData} from "test/pumps/PumpHelpers.sol"; import {MockReserveWell} from "mocks/wells/MockReserveWell.sol"; import {IMultiFlowPumpErrors} from "src/interfaces/pumps/IMultiFlowPumpErrors.sol"; import {from18} from "test/pumps/PumpHelpers.sol"; contract PumpNotInitialized is TestHelper { MultiFlowPump pump; + bytes data; MockReserveWell mWell; uint256[] b = new uint256[](2); function setUp() public { mWell = new MockReserveWell(); initUser(); - pump = new MultiFlowPump( - from18(0.5e18), - from18(0.333333333333333333e18), - 12, - from18(0.9e18) - ); + pump = new MultiFlowPump(); uint256[] memory reserves = new uint256[](2); mWell.setReserves(reserves); + data = mockPumpData(); } function test_not_initialized_last_cumulative_reserves() public { vm.expectRevert(IMultiFlowPumpErrors.NotInitialized.selector); - pump.readLastCumulativeReserves(address(mWell)); + pump.readLastCumulativeReserves(address(mWell), data); } function test_not_initialized_cumulative_reserves() public { vm.expectRevert(IMultiFlowPumpErrors.NotInitialized.selector); - pump.readCumulativeReserves(address(mWell), new bytes(0)); + pump.readCumulativeReserves(address(mWell), data); } function test_not_initialized_last_instantaneous_reserves() public { vm.expectRevert(IMultiFlowPumpErrors.NotInitialized.selector); - pump.readLastInstantaneousReserves(address(mWell)); + pump.readLastInstantaneousReserves(address(mWell), data); } function test_not_initialized_instantaneous_reserves() public { vm.expectRevert(IMultiFlowPumpErrors.NotInitialized.selector); - pump.readInstantaneousReserves(address(mWell), new bytes(0)); + pump.readInstantaneousReserves(address(mWell), data); } function test_not_initialized_last_capped_reserves() public { vm.expectRevert(IMultiFlowPumpErrors.NotInitialized.selector); - pump.readLastCappedReserves(address(mWell)); + pump.readLastCappedReserves(address(mWell), data); } function test_not_initialized_capped_reserves() public { vm.expectRevert(IMultiFlowPumpErrors.NotInitialized.selector); - pump.readCappedReserves(address(mWell)); + pump.readCappedReserves(address(mWell), data); } function test_not_initialized_twa_reserves() public { vm.expectRevert(IMultiFlowPumpErrors.NotInitialized.selector); - pump.readTwaReserves(address(mWell), new bytes(0), 0, new bytes(0)); + pump.readTwaReserves(address(mWell), data, 0, data); } } diff --git a/test/pumps/Pump.TimeWeightedAverage.t.sol b/test/pumps/Pump.TimeWeightedAverage.t.sol index 79a62a17..1d79f595 100644 --- a/test/pumps/Pump.TimeWeightedAverage.t.sol +++ b/test/pumps/Pump.TimeWeightedAverage.t.sol @@ -3,8 +3,9 @@ pragma solidity ^0.8.20; import {console, TestHelper} from "test/TestHelper.sol"; import {MultiFlowPump, ABDKMathQuad} from "src/pumps/MultiFlowPump.sol"; -import {from18, to18} from "test/pumps/PumpHelpers.sol"; +import {mockPumpData, from18, to18} from "test/pumps/PumpHelpers.sol"; import {MockReserveWell} from "mocks/wells/MockReserveWell.sol"; +import {ConstantProduct2} from "src/functions/ConstantProduct2.sol"; import {log2, powu, UD60x18, wrap, unwrap} from "prb/math/UD60x18.sol"; import {exp2, log2, powu, UD60x18, wrap, unwrap, uUNIT} from "prb/math/UD60x18.sol"; @@ -13,6 +14,7 @@ contract PumpTimeWeightedAverageTest is TestHelper { using ABDKMathQuad for bytes16; MultiFlowPump pump; + bytes data; MockReserveWell mWell; uint256[] b = new uint256[](2); @@ -22,19 +24,17 @@ contract PumpTimeWeightedAverageTest is TestHelper { function setUp() public { mWell = new MockReserveWell(); initUser(); - pump = new MultiFlowPump( - from18(0.5e18), // cap reserves if changed +/- 50% per block - from18(0.5e18), // cap reserves if changed +/- 50% per block - 12, // block time - from18(0.9e18) // ema alpha - ); + pump = new MultiFlowPump(); + data = mockPumpData(); + wellFunction.target = address(new ConstantProduct2()); + mWell.setWellFunction(wellFunction); // Send first update to the Pump, which will initialize it vm.prank(user); b[0] = 1e6; b[1] = 2e6; - mWell.update(address(pump), b, new bytes(0)); - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); + mWell.update(address(pump), b, data); uint256[] memory checkReserves = mWell.getReserves(); assertEq(checkReserves[0], b[0]); @@ -44,8 +44,8 @@ contract PumpTimeWeightedAverageTest is TestHelper { function testTWAReserves() public prank(user) { increaseTime(12); - bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), ""); - uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell)); + bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), data); + uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell), data); assertApproxEqAbs(lastReserves[0], 1e6, 1); assertApproxEqAbs(lastReserves[1], 2e6, 1); @@ -53,18 +53,18 @@ contract PumpTimeWeightedAverageTest is TestHelper { increaseTime(120); uint256[] memory twaReserves; - (twaReserves,) = pump.readTwaReserves(address(mWell), startCumulativeReserves, block.timestamp - 120, ""); + (twaReserves,) = pump.readTwaReserves(address(mWell), startCumulativeReserves, block.timestamp - 120, data); assertApproxEqAbs(twaReserves[0], 1e6, 1); assertApproxEqAbs(twaReserves[1], 2e6, 1); b[0] = 2e6; b[1] = 4e6; - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); increaseTime(120); - (twaReserves,) = pump.readTwaReserves(address(mWell), startCumulativeReserves, block.timestamp - 240, ""); + (twaReserves,) = pump.readTwaReserves(address(mWell), startCumulativeReserves, block.timestamp - 240, data); assertEq(twaReserves[0], 1_414_213); // Geometric Mean of 1e6 and 2e6 is 1_414_213 assertEq(twaReserves[1], 2_828_427); // Geometric mean of 2e6 and 4e6 is 2_828_427 diff --git a/test/pumps/Pump.Update.t.sol b/test/pumps/Pump.Update.t.sol index 2329e258..acc5f4c3 100644 --- a/test/pumps/Pump.Update.t.sol +++ b/test/pumps/Pump.Update.t.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.20; import {console, TestHelper} from "test/TestHelper.sol"; import {MultiFlowPump, ABDKMathQuad} from "src/pumps/MultiFlowPump.sol"; -import {from18, to18} from "test/pumps/PumpHelpers.sol"; +import {ConstantProduct2} from "src/functions/ConstantProduct2.sol"; +import {from18, to18, mockPumpData} from "test/pumps/PumpHelpers.sol"; import {MockReserveWell} from "mocks/wells/MockReserveWell.sol"; import {IMultiFlowPumpErrors} from "src/interfaces/pumps/IMultiFlowPumpErrors.sol"; @@ -14,6 +15,7 @@ contract PumpUpdateTest is TestHelper { using ABDKMathQuad for bytes16; MultiFlowPump pump; + bytes data; MockReserveWell mWell; uint256[] b = new uint256[](2); @@ -23,278 +25,283 @@ contract PumpUpdateTest is TestHelper { function setUp() public { mWell = new MockReserveWell(); initUser(); - pump = new MultiFlowPump( - from18(0.5e18), // cap reserves if changed +/- 50% per block - from18(0.5e18), // cap reserves if changed +/- 50% per block - 12, // block time - from18(0.9e18) // ema alpha - ); + pump = new MultiFlowPump(); + data = mockPumpData(); + wellFunction.target = address(new ConstantProduct2()); + mWell.setWellFunction(wellFunction); // Send first update to the Pump, which will initialize it vm.prank(user); b[0] = 1e6; b[1] = 2e6; - mWell.update(address(pump), b, new bytes(0)); - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); + mWell.update(address(pump), b, data); } function test_initialized() public prank(user) { - bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), new bytes(0)); + bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), data); uint256 lastTimestamp = block.timestamp; // Last reserves are initialized with initial liquidity - uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell)); + uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell), data); assertApproxEqAbs(lastReserves[0], 1e6, 1); assertApproxEqAbs(lastReserves[1], 2e6, 1); // - uint256[] memory lastEmaReserves = pump.readLastInstantaneousReserves(address(mWell)); + uint256[] memory lastEmaReserves = pump.readLastInstantaneousReserves(address(mWell), data); assertApproxEqAbs(lastEmaReserves[0], 1e6, 1); assertApproxEqAbs(lastEmaReserves[1], 2e6, 1); // EMA reserves are initialized with initial liquidity - uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), new bytes(0)); + uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), data); assertApproxEqAbs(emaReserves[0], 1e6, 1); assertApproxEqAbs(emaReserves[1], 2e6, 1); // Cumulative reserves are initialized to zero - bytes16[] memory lastCumulativeReserves = pump.readLastCumulativeReserves(address(mWell)); + bytes16[] memory lastCumulativeReserves = pump.readLastCumulativeReserves(address(mWell), data); assertEq(lastCumulativeReserves[0], bytes16(0)); assertEq(lastCumulativeReserves[1], bytes16(0)); - bytes16[] memory cumulativeReserves = - abi.decode(pump.readCumulativeReserves(address(mWell), new bytes(0)), (bytes16[])); + bytes16[] memory cumulativeReserves = abi.decode(pump.readCumulativeReserves(address(mWell), data), (bytes16[])); assertEq(cumulativeReserves[0], bytes16(0)); assertEq(cumulativeReserves[1], bytes16(0)); vm.expectRevert(IMultiFlowPumpErrors.NoTimePassed.selector); - pump.readTwaReserves(address(mWell), startCumulativeReserves, lastTimestamp, new bytes(0)); + pump.readTwaReserves(address(mWell), startCumulativeReserves, lastTimestamp, data); } /// @dev no time has elapsed since prev update = no change function test_update_0Seconds() public prank(user) { - bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), new bytes(0)); + bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), data); uint256 lastTimestamp = block.timestamp; b[0] = 2e6; b[1] = 1e6; - mWell.update(address(pump), b, new bytes(0)); - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); + mWell.update(address(pump), b, data); - uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell)); + uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell), data); assertApproxEqAbs(lastReserves[0], 1e6, 1); assertApproxEqAbs(lastReserves[1], 2e6, 1); - uint256[] memory lastEmaReserves = pump.readLastInstantaneousReserves(address(mWell)); + uint256[] memory lastEmaReserves = pump.readLastInstantaneousReserves(address(mWell), data); assertApproxEqAbs(lastEmaReserves[0], 1e6, 1); assertApproxEqAbs(lastEmaReserves[1], 2e6, 1); - uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), new bytes(0)); + uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), data); assertApproxEqAbs(emaReserves[0], 1e6, 1); assertApproxEqAbs(emaReserves[1], 2e6, 1); - bytes16[] memory lastCumulativeReserves = pump.readLastCumulativeReserves(address(mWell)); + bytes16[] memory lastCumulativeReserves = pump.readLastCumulativeReserves(address(mWell), data); assertEq(lastCumulativeReserves[0], bytes16(0)); assertEq(lastCumulativeReserves[1], bytes16(0)); - bytes16[] memory cumulativeReserves = - abi.decode(pump.readCumulativeReserves(address(mWell), new bytes(0)), (bytes16[])); + bytes16[] memory cumulativeReserves = abi.decode(pump.readCumulativeReserves(address(mWell), data), (bytes16[])); assertEq(cumulativeReserves[0], bytes16(0)); assertEq(cumulativeReserves[1], bytes16(0)); vm.expectRevert(IMultiFlowPumpErrors.NoTimePassed.selector); - pump.readTwaReserves(address(mWell), startCumulativeReserves, lastTimestamp, new bytes(0)); + pump.readTwaReserves(address(mWell), startCumulativeReserves, lastTimestamp, data); } function test_update_12Seconds() public prank(user) { - bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), new bytes(0)); + bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), data); // After CAP_INTERVAL, Pump receives an update b[0] = 2e6; // 1e6 -> 2e6 = +100% b[1] = 1e6; // 2e6 -> 1e6 = - 50% - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); increaseTime(CAP_INTERVAL); - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); // - uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell)); - assertApproxEqAbs(lastReserves[0], 1.5e6, 1); // capped - assertApproxEqAbs(lastReserves[1], 1e6, 1); // uncapped + uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell), data); + assertApproxEqAbs(lastReserves[0], 1_224_743, 1); + assertApproxEqAbs(lastReserves[1], 1_632_992, 1); // - uint256[] memory lastEmaReserves = pump.readLastInstantaneousReserves(address(mWell)); - assertEq(lastEmaReserves[0], 1_337_697); - assertEq(lastEmaReserves[1], 1_216_241); + uint256[] memory lastEmaReserves = pump.readLastInstantaneousReserves(address(mWell), data); + assertApproxEqAbs(lastEmaReserves[0], 1_156_587, 1); // = 2^(log2(1000000) * 0.9^12 +log2(1224743) * (1-0.9^12)) + assertApproxEqAbs(lastEmaReserves[1], 1_729_223, 1); // = 2^(log2(2000000) * 0.9^12 +log2(1632992) * (1-0.9^12)) // - uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), new bytes(0)); - assertEq(emaReserves[0], 1_337_697); - assertEq(emaReserves[1], 1_216_241); + uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), data); + 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)) // - bytes16[] memory lastCumulativeReserves = pump.readLastCumulativeReserves(address(mWell)); - assertApproxEqAbs(lastCumulativeReserves[0].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1.5e6, 1); - assertApproxEqAbs(lastCumulativeReserves[1].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1e6, 1); + bytes16[] memory lastCumulativeReserves = pump.readLastCumulativeReserves(address(mWell), data); + assertApproxEqAbs(lastCumulativeReserves[0].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1_224_743, 1); + assertApproxEqAbs(lastCumulativeReserves[1].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1_632_992, 1); - bytes16[] memory cumulativeReserves = - abi.decode(pump.readCumulativeReserves(address(mWell), new bytes(0)), (bytes16[])); - assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1.5e6, 1); - assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1e6, 1); + 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); (uint256[] memory twaReserves, bytes memory twaCumulativeReservesBytes) = - pump.readTwaReserves(address(mWell), startCumulativeReserves, block.timestamp - CAP_INTERVAL, new bytes(0)); + pump.readTwaReserves(address(mWell), startCumulativeReserves, block.timestamp - CAP_INTERVAL, data); - assertApproxEqAbs(twaReserves[0], 1.5e6, 1); - assertApproxEqAbs(twaReserves[1], 1e6, 1); + assertApproxEqAbs(twaReserves[0], 1_224_743, 1); + assertApproxEqAbs(twaReserves[1], 1_632_992, 1); cumulativeReserves = abi.decode(twaCumulativeReservesBytes, (bytes16[])); - assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1.5e6, 1); - assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1e6, 1); + 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); } function test_12Seconds_read() public prank(user) { - bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), new bytes(0)); + bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), data); b[0] = 2e6; // 1e6 -> 2e6 = +100% b[1] = 1e6; // 2e6 -> 1e6 = - 50% - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); increaseTime(CAP_INTERVAL); - uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), new bytes(0)); - assertEq(emaReserves[0], 1_337_697); - assertEq(emaReserves[1], 1_216_241); + 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), new bytes(0)), (bytes16[])); - assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1.5e6, 1); - assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1e6, 1); + 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, new bytes(0)); + pump.readTwaReserves(address(mWell), startCumulativeReserves, block.timestamp - CAP_INTERVAL, data); - assertApproxEqAbs(twaReserves[0], 1.5e6, 1); - assertApproxEqAbs(twaReserves[1], 1e6, 1); + assertEq(twaReserves.length, 2); + assertApproxEqAbs(twaReserves[0], 1_224_743, 1); + assertApproxEqAbs(twaReserves[1], 1_632_992, 1); cumulativeReserves = abi.decode(twaCumulativeReservesBytes, (bytes16[])); - assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1.5e6, 1); - assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(12)).pow_2().toUInt(), 1e6, 1); + assertEq(cumulativeReserves.length, 2); + 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); } function test_12seconds_update_12Seconds_update() public prank(user) { - bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), new bytes(0)); + bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), data); b[0] = 2e6; // 1e6 -> 2e6 = +100% b[1] = 1e6; // 2e6 -> 1e6 = - 50% - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); increaseTime(CAP_INTERVAL); - bytes memory startCumulativeReserves2 = pump.readCumulativeReserves(address(mWell), new bytes(0)); + bytes memory startCumulativeReserves2 = pump.readCumulativeReserves(address(mWell), data); - b[0] = 1e6; // 1e6 -> 2e6 = +100% - b[1] = 2e6; // 2e6 -> 1e6 = - 50% + b[0] = 1e6; // 1e6 -> 2e6 = - 50% + b[1] = 2e6; // 2e6 -> 1e6 = +100% - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); increaseTime(CAP_INTERVAL); - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); // - uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell)); + uint256[] memory lastReserves = pump.readLastCappedReserves(address(mWell), data); assertApproxEqAbs(lastReserves[0], 1e6, 1); // capped - assertApproxEqAbs(lastReserves[1], 1.5e6, 1); // uncapped + assertApproxEqAbs(lastReserves[1], 2e6, 1); // uncapped // - uint256[] memory lastEmaReserves = pump.readLastInstantaneousReserves(address(mWell)); - assertEq(lastEmaReserves[0], 1_085_643); - assertEq(lastEmaReserves[1], 1_413_741); + uint256[] memory lastEmaReserves = pump.readLastInstantaneousReserves(address(mWell), data); + assertEq(lastEmaReserves[0], 1_041_941); // = 2^(log2(1156587) * 0.9^12 +log2(1000000) * (1-0.9^12)) + assertEq(lastEmaReserves[1], 1_919_492); // = 2^(log2(1729223) * 0.9^12 +log2(2000000) * (1-0.9^12)) // - uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), new bytes(0)); - assertEq(emaReserves[0], 1_085_643); - assertEq(emaReserves[1], 1_413_741); + uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), data); + assertEq(emaReserves[0], 1_041_941); // = 2^(log2(1156587) * 0.9^12 +log2(1000000) * (1-0.9^12)) + assertEq(emaReserves[1], 1_919_492); // = 2^(log2(1729223) * 0.9^12 +log2(2000000) * (1-0.9^12)) // - bytes16[] memory lastCumulativeReserves = pump.readLastCumulativeReserves(address(mWell)); - assertApproxEqAbs(lastCumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); - assertApproxEqAbs(lastCumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); + bytes16[] memory lastCumulativeReserves = pump.readLastCumulativeReserves(address(mWell), data); + assertApproxEqAbs(lastCumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_106_681, 1); // = 2^((log2(1632992) * 12 + log2(2000000) * 12) / (12 + 12)) + assertApproxEqAbs(lastCumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_807_203, 1); // = 2^((log2(1224743) * 12 + log2(1000000) * 12) / (12 + 12)) - bytes16[] memory cumulativeReserves = - abi.decode(pump.readCumulativeReserves(address(mWell), new bytes(0)), (bytes16[])); - assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); - assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); + bytes16[] memory cumulativeReserves = abi.decode(pump.readCumulativeReserves(address(mWell), data), (bytes16[])); + assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_106_681, 1); // = 2^((log2(1632992) * 12 + log2(2000000) * 12) / (12 + 12)) + assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_807_203, 1); // = 2^((log2(1224743) * 12 + log2(1000000) * 12) / (12 + 12)) - (uint256[] memory twaReserves, bytes memory twaCumulativeReservesBytes) = pump.readTwaReserves( - address(mWell), startCumulativeReserves, block.timestamp - 2 * CAP_INTERVAL, new bytes(0) - ); + (uint256[] memory twaReserves, bytes memory twaCumulativeReservesBytes) = + pump.readTwaReserves(address(mWell), startCumulativeReserves, block.timestamp - 2 * CAP_INTERVAL, data); - assertApproxEqAbs(twaReserves[0], 1_224_744, 1); - assertApproxEqAbs(twaReserves[1], 1_224_744, 1); + assertApproxEqAbs(twaReserves[0], 1_106_681, 1); // = 2^((log2(1632992) * 12 + log2(2000000) * 12) / (12 + 12)) + assertApproxEqAbs(twaReserves[1], 1_807_203, 1); // = 2^((log2(1224743) * 12 + log2(1000000) * 12) / (12 + 12)) cumulativeReserves = abi.decode(twaCumulativeReservesBytes, (bytes16[])); - assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); - assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); + assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_106_681, 1); // = 2^((log2(1632992) * 12 + log2(2000000) * 12) / (12 + 12)) + assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_807_203, 1); // = 2^((log2(1224743) * 12 + log2(1000000) * 12) / (12 + 12)) (twaReserves, twaCumulativeReservesBytes) = - pump.readTwaReserves(address(mWell), startCumulativeReserves2, block.timestamp - CAP_INTERVAL, new bytes(0)); + pump.readTwaReserves(address(mWell), startCumulativeReserves2, block.timestamp - CAP_INTERVAL, data); assertApproxEqAbs(twaReserves[0], 1e6, 1); - assertApproxEqAbs(twaReserves[1], 1.5e6, 1); + assertApproxEqAbs(twaReserves[1], 2e6, 1); cumulativeReserves = abi.decode(twaCumulativeReservesBytes, (bytes16[])); - assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); - assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); + assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_106_681, 1); // = 2^((log2(1632992) * 12 + log2(2000000) * 12) / (12 + 12)) + assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_807_203, 1); // = 2^((log2(1224743) * 12 + log2(1000000) * 12) / (12 + 12)) } function test_12seconds_update_12Seconds_read() public prank(user) { - bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), new bytes(0)); + bytes memory startCumulativeReserves = pump.readCumulativeReserves(address(mWell), data); b[0] = 2e6; // 1e6 -> 2e6 = +100% b[1] = 1e6; // 2e6 -> 1e6 = - 50% - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); increaseTime(CAP_INTERVAL); - bytes memory startCumulativeReserves2 = pump.readCumulativeReserves(address(mWell), new bytes(0)); + bytes memory startCumulativeReserves2 = pump.readCumulativeReserves(address(mWell), data); - b[0] = 1e6; // 1e6 -> 2e6 = +100% - b[1] = 2e6; // 2e6 -> 1e6 = - 50% + b[0] = 1e6; // 1e6 -> 2e6 = - 50% + b[1] = 2e6; // 2e6 -> 1e6 = +100% - mWell.update(address(pump), b, new bytes(0)); + mWell.update(address(pump), b, data); increaseTime(CAP_INTERVAL); - uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), new bytes(0)); - assertEq(emaReserves[0], 1_085_643); - assertEq(emaReserves[1], 1_413_741); + uint256[] memory lastReserves = pump.readCappedReserves(address(mWell), data); - bytes16[] memory cumulativeReserves = - abi.decode(pump.readCumulativeReserves(address(mWell), new bytes(0)), (bytes16[])); - assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); - assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); + assertApproxEqAbs(lastReserves[0], 1e6, 1); + assertApproxEqAbs(lastReserves[1], 2e6, 1); - (uint256[] memory twaReserves, bytes memory twaCumulativeReservesBytes) = pump.readTwaReserves( - address(mWell), startCumulativeReserves, block.timestamp - 2 * CAP_INTERVAL, new bytes(0) - ); + uint256[] memory emaReserves = pump.readInstantaneousReserves(address(mWell), data); + assertEq(emaReserves[0], 1_041_941); // = 2^(log2(1156587) * 0.9^12 +log2(1000000) * (1-0.9^12)) + assertEq(emaReserves[1], 1_919_492); // = 2^(log2(1729223) * 0.9^12 +log2(2000000) * (1-0.9^12)) + + bytes16[] memory cumulativeReserves = abi.decode(pump.readCumulativeReserves(address(mWell), data), (bytes16[])); + assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_106_681, 1); // = 2^((log2(1632992) * 12 + log2(2000000) * 12) / (12 + 12)) + assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_807_203, 1); // = 2^((log2(1224743) * 12 + log2(1000000) * 12) / (12 + 12)) + + (uint256[] memory twaReserves, bytes memory twaCumulativeReservesBytes) = + pump.readTwaReserves(address(mWell), startCumulativeReserves, block.timestamp - 2 * CAP_INTERVAL, data); - assertApproxEqAbs(twaReserves[0], 1_224_744, 1); - assertApproxEqAbs(twaReserves[1], 1_224_744, 1); + assertApproxEqAbs(twaReserves[0], 1_106_681, 1); // = 2^((log2(1632992) * 12 + log2(2000000) * 12) / (12 + 12)) + assertApproxEqAbs(twaReserves[1], 1_807_203, 1); // = 2^((log2(1224743) * 12 + log2(1000000) * 12) / (12 + 12)) cumulativeReserves = abi.decode(twaCumulativeReservesBytes, (bytes16[])); - assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); - assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); + assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_106_681, 1); // = 2^((log2(1632992) * 12 + log2(2000000) * 12) / (12 + 12)) + assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_807_203, 1); // = 2^((log2(1224743) * 12 + log2(1000000) * 12) / (12 + 12)) (twaReserves, twaCumulativeReservesBytes) = - pump.readTwaReserves(address(mWell), startCumulativeReserves2, block.timestamp - CAP_INTERVAL, new bytes(0)); + pump.readTwaReserves(address(mWell), startCumulativeReserves2, block.timestamp - CAP_INTERVAL, data); assertApproxEqAbs(twaReserves[0], 1e6, 1); - assertApproxEqAbs(twaReserves[1], 1.5e6, 1); + assertApproxEqAbs(twaReserves[1], 2e6, 1); cumulativeReserves = abi.decode(twaCumulativeReservesBytes, (bytes16[])); - assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); - assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_224_744, 1); + assertApproxEqAbs(cumulativeReserves[0].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_106_681, 1); // = 2^((log2(1632992) * 12 + log2(2000000) * 12) / (12 + 12)) + assertApproxEqAbs(cumulativeReserves[1].div(ABDKMathQuad.fromUInt(24)).pow_2().toUInt(), 1_807_203, 1); // = 2^((log2(1224743) * 12 + log2(1000000) * 12) / (12 + 12)) } } diff --git a/test/pumps/PumpHelpers.sol b/test/pumps/PumpHelpers.sol index 1bf3aebc..24dca9d3 100644 --- a/test/pumps/PumpHelpers.sol +++ b/test/pumps/PumpHelpers.sol @@ -3,16 +3,17 @@ pragma solidity >=0.8.0; import {ABDKMathQuad} from "src/libraries/ABDKMathQuad.sol"; import {console} from "forge-std/Test.sol"; +import {MultiFlowPump} from "src/pumps/MultiFlowPump.sol"; uint256 constant MAX_128 = 2 ** 128; uint256 constant MAX_E18 = 1e18; function from18(uint256 a) pure returns (bytes16 result) { - return ABDKMathQuad.from128x128(int256(a * MAX_128 / MAX_E18)); + return ABDKMathQuad.from128x128(int256((a * MAX_128) / MAX_E18)); } function to18(bytes16 a) pure returns (uint256 result) { - return uint256(ABDKMathQuad.to128x128(a)) * MAX_E18 / MAX_128; + return (uint256(ABDKMathQuad.to128x128(a)) * MAX_E18) / MAX_128; } function simCapReserve50Percent( @@ -27,7 +28,7 @@ function simCapReserve50Percent( uint256 tempReserve; for (uint256 i; i < blocks; ++i) { unchecked { - tempReserve = limitReserve * multiplier / 1e6; + tempReserve = (limitReserve * multiplier) / 1e6; } if (lastReserve < reserve && tempReserve < limitReserve) { limitReserve = type(uint256).max; @@ -54,7 +55,7 @@ function generateRandomUpdate( timeIncrease = uint40(uint256(seed)) % 50_000_000; for (uint256 i; i < n; ++i) { seed = stepSeed(seed); - balances[i] = uint256(uint128(uint256(seed))); // case to uint128 + balances[i] = uint256(seed) % 1e32; // } newSeed = seed; } @@ -62,3 +63,23 @@ function generateRandomUpdate( function stepSeed(bytes32 seed) pure returns (bytes32 newSeed) { newSeed = keccak256(abi.encode(seed)); } + +function encodePumpData( + bytes16 alpha, + uint256 capInterval, + MultiFlowPump.CapReservesParameters memory crp +) pure returns (bytes memory data) { + data = abi.encode(alpha, capInterval, crp); +} + +function mockPumpData() pure returns (bytes memory data) { + bytes16[][] memory maxRateChanges = new bytes16[][](2); + maxRateChanges[0] = new bytes16[](2); + maxRateChanges[1] = new bytes16[](2); + maxRateChanges[0][1] = from18(0.5e18); + maxRateChanges[1][0] = from18(0.5e18); + + data = encodePumpData( + from18(0.9e18), 12, MultiFlowPump.CapReservesParameters(maxRateChanges, from18(0.5e18), from18(0.4761904762e18)) + ); +}