From 83f678fd579e2b2d4e5ed7033277c964ad94993e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 9 Oct 2024 19:13:15 -0300 Subject: [PATCH 01/52] Gyro 2-CLP --- pkg/pool-gyro/.solcover.js | 3 + pkg/pool-gyro/.solhintignore | 1 + pkg/pool-gyro/CHANGELOG.md | 3 + pkg/pool-gyro/README.md | 13 + pkg/pool-gyro/contracts/Gyro2CLPPool.sol | 213 ++++++++++++++++ .../contracts/Gyro2CLPPoolFactory.sol | 75 ++++++ pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol | 227 ++++++++++++++++++ pkg/pool-gyro/contracts/lib/GyroPoolMath.sol | 139 +++++++++++ pkg/pool-gyro/coverage.sh | 124 ++++++++++ pkg/pool-gyro/foundry.toml | 50 ++++ pkg/pool-gyro/hardhat.config.ts | 20 ++ pkg/pool-gyro/package.json | 60 +++++ .../test/foundry/ComputeBalance.t.sol | 81 +++++++ .../foundry/LiquidityApproximationGyro.t.sol | 51 ++++ pkg/pool-gyro/tsconfig.json | 6 + yarn.lock | 26 ++ 16 files changed, 1092 insertions(+) create mode 100644 pkg/pool-gyro/.solcover.js create mode 100644 pkg/pool-gyro/.solhintignore create mode 100644 pkg/pool-gyro/CHANGELOG.md create mode 100644 pkg/pool-gyro/README.md create mode 100644 pkg/pool-gyro/contracts/Gyro2CLPPool.sol create mode 100644 pkg/pool-gyro/contracts/Gyro2CLPPoolFactory.sol create mode 100644 pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol create mode 100644 pkg/pool-gyro/contracts/lib/GyroPoolMath.sol create mode 100755 pkg/pool-gyro/coverage.sh create mode 100755 pkg/pool-gyro/foundry.toml create mode 100644 pkg/pool-gyro/hardhat.config.ts create mode 100644 pkg/pool-gyro/package.json create mode 100644 pkg/pool-gyro/test/foundry/ComputeBalance.t.sol create mode 100644 pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol create mode 100644 pkg/pool-gyro/tsconfig.json diff --git a/pkg/pool-gyro/.solcover.js b/pkg/pool-gyro/.solcover.js new file mode 100644 index 000000000..e06da7cac --- /dev/null +++ b/pkg/pool-gyro/.solcover.js @@ -0,0 +1,3 @@ +module.exports = { + skipFiles: ['test'], +}; diff --git a/pkg/pool-gyro/.solhintignore b/pkg/pool-gyro/.solhintignore new file mode 100644 index 000000000..11d11fbae --- /dev/null +++ b/pkg/pool-gyro/.solhintignore @@ -0,0 +1 @@ +contracts/test/ diff --git a/pkg/pool-gyro/CHANGELOG.md b/pkg/pool-gyro/CHANGELOG.md new file mode 100644 index 000000000..1512c4216 --- /dev/null +++ b/pkg/pool-gyro/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +## Unreleased diff --git a/pkg/pool-gyro/README.md b/pkg/pool-gyro/README.md new file mode 100644 index 000000000..64d737619 --- /dev/null +++ b/pkg/pool-gyro/README.md @@ -0,0 +1,13 @@ +# Balancer + +# Balancer V3 Gyro Pools + + +## Overview + + +### Usage + +## Licensing + +[GNU General Public License Version 3 (GPL v3)](../../LICENSE). diff --git a/pkg/pool-gyro/contracts/Gyro2CLPPool.sol b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol new file mode 100644 index 000000000..798a8d2bc --- /dev/null +++ b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: LicenseRef-Gyro-1.0 +// for information on licensing please see the README in the GitHub repository +// . + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; +import { ISwapFeePercentageBounds } from "@balancer-labs/v3-interfaces/contracts/vault/ISwapFeePercentageBounds.sol"; +import { + IUnbalancedLiquidityInvariantRatioBounds +} from "@balancer-labs/v3-interfaces/contracts/vault/IUnbalancedLiquidityInvariantRatioBounds.sol"; +import { BalancerPoolToken } from "@balancer-labs/v3-vault/contracts/BalancerPoolToken.sol"; +import { PoolSwapParams, Rounding, SwapKind } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import "./lib/GyroPoolMath.sol"; +import "./lib/Gyro2CLPMath.sol"; + +contract Gyro2CLPPool is IBasePool, BalancerPoolToken { + using FixedPoint for uint256; + + uint256 private immutable _sqrtAlpha; + uint256 private immutable _sqrtBeta; + + bytes32 private constant _POOL_TYPE = "2CLP"; + + struct GyroParams { + string name; + string symbol; + uint256 sqrtAlpha; // A: Should already be upscaled + uint256 sqrtBeta; // A: Should already be upscaled. Could be passed as an array[](2) + } + + error SqrtParamsWrong(); + error SupportsOnlyTwoTokens(); + error NotImplemented(); + + constructor(GyroParams memory params, IVault vault) BalancerPoolToken(vault, params.name, params.symbol) { + if (params.sqrtAlpha >= params.sqrtBeta) { + revert SqrtParamsWrong(); + } + + _sqrtAlpha = params.sqrtAlpha; + _sqrtBeta = params.sqrtBeta; + } + + /// @inheritdoc IBasePool + function computeInvariant(uint256[] memory balancesLiveScaled18, Rounding rounding) public view returns (uint256) { + uint256[2] memory sqrtParams = _sqrtParameters(); + + return Gyro2CLPMath._calculateInvariant(balancesLiveScaled18, sqrtParams[0], sqrtParams[1], rounding); + } + + /// @inheritdoc IBasePool + function computeBalance( + uint256[] memory balancesLiveScaled18, + uint256 tokenInIndex, + uint256 invariantRatio + ) external view returns (uint256 newBalance) { + /********************************************************************************************** + // Gyro invariant formula is: + // Lˆ2 = (x + a)(y + b) + // where: + // a = L / _sqrtBeta + // b = L * _sqrtAlpha + // + // In computeBalance, we want to know what's the new balance of a token, given that invariant + // changed and the other token balance didn't change. To calculate that for "x", we use: + // + // (L*Lratio)ˆ2 = (newX + (L*Lratio) / _sqrtBeta)(y + (L*Lratio) * _sqrtAlpha) + // + // To simplify, let's rename a few terms: + // + // squareNewInv = (newX + a)(y + b) + // + // Isolating newX: newX = (squareNewInv/(y + b)) - a + // For newY: newY = (squareNewInv/(x + a)) - b + **********************************************************************************************/ + + uint256[2] memory sqrtParams = _sqrtParameters(); + uint256 invariant = Gyro2CLPMath._calculateInvariant( + balancesLiveScaled18, + sqrtParams[0], + sqrtParams[1], + Rounding.ROUND_DOWN + ); + // New invariant + invariant = invariant.mulUp(invariantRatio); + uint256 squareNewInv = invariant * invariant; + // L / sqrt(beta) + uint256 a = invariant.divUp(sqrtParams[1]); + // L * sqrt(alpha) + uint256 b = invariant.mulUp(sqrtParams[0]); + + if (tokenInIndex == 0) { + // if newBalance = newX + newBalance = squareNewInv.divUpRaw(b + balancesLiveScaled18[1]) - a; + } else { + // if newBalance = newY + newBalance = squareNewInv.divUpRaw(a + balancesLiveScaled18[0]) - b; + } + } + + /// @inheritdoc IBasePool + function onSwap(PoolSwapParams calldata request) public view onlyVault returns (uint256) { + bool tokenInIsToken0 = request.indexIn == 0; + uint256 balanceTokenInScaled18 = request.balancesScaled18[request.indexIn]; + uint256 balanceTokenOutScaled18 = request.balancesScaled18[request.indexOut]; + + // All the calculations in one function to avoid Error Stack Too Deep + (, uint256 virtualParamIn, uint256 virtualParamOut) = _calculateCurrentValues( + balanceTokenInScaled18, + balanceTokenOutScaled18, + tokenInIsToken0, + request.kind == SwapKind.EXACT_IN ? Rounding.ROUND_DOWN : Rounding.ROUND_UP + ); + + if (request.kind == SwapKind.EXACT_IN) { + uint256 amountOutScaled18 = Gyro2CLPMath._calcOutGivenIn( + balanceTokenInScaled18, + balanceTokenOutScaled18, + request.amountGivenScaled18, + virtualParamIn, + virtualParamOut + ); + + return amountOutScaled18; + } else { + uint256 amountInScaled18 = Gyro2CLPMath._calcInGivenOut( + balanceTokenInScaled18, + balanceTokenOutScaled18, + request.amountGivenScaled18, + virtualParamIn, + virtualParamOut + ); + + // Fees are added after scaling happens, to reduce the complexity of the rounding direction analysis. + return amountInScaled18; + } + } + + function _sqrtParameters() internal view virtual returns (uint256[2] memory virtualParameters) { + virtualParameters[0] = _sqrtParameters(true); + virtualParameters[1] = _sqrtParameters(false); + return virtualParameters; + } + + function _sqrtParameters(bool parameter0) internal view virtual returns (uint256) { + return parameter0 ? _sqrtAlpha : _sqrtBeta; + } + + function _getVirtualParameters( + uint256[2] memory sqrtParams, + uint256 invariant + ) internal view virtual returns (uint256[2] memory virtualParameters) { + virtualParameters[0] = _virtualParameters(true, sqrtParams[1], invariant); + virtualParameters[1] = _virtualParameters(false, sqrtParams[0], invariant); + return virtualParameters; + } + + function _virtualParameters( + bool parameter0, + uint256 sqrtParam, + uint256 invariant + ) internal view virtual returns (uint256) { + return + parameter0 + ? (Gyro2CLPMath._calculateVirtualParameter0(invariant, sqrtParam)) + : (Gyro2CLPMath._calculateVirtualParameter1(invariant, sqrtParam)); + } + + function _calculateCurrentValues( + uint256 balanceTokenInScaled18, + uint256 balanceTokenOutScaled18, + bool tokenInIsToken0, + Rounding rounding + ) internal view returns (uint256 currentInvariant, uint256 virtualParamIn, uint256 virtualParamOut) { + uint256[] memory balances = new uint256[](2); + balances[0] = tokenInIsToken0 ? balanceTokenInScaled18 : balanceTokenOutScaled18; + balances[1] = tokenInIsToken0 ? balanceTokenOutScaled18 : balanceTokenInScaled18; + + uint256[2] memory sqrtParams = _sqrtParameters(); + + currentInvariant = Gyro2CLPMath._calculateInvariant(balances, sqrtParams[0], sqrtParams[1], rounding); + + uint256[2] memory virtualParam = _getVirtualParameters(sqrtParams, currentInvariant); + + virtualParamIn = tokenInIsToken0 ? virtualParam[0] : virtualParam[1]; + virtualParamOut = tokenInIsToken0 ? virtualParam[1] : virtualParam[0]; + } + + /// @inheritdoc ISwapFeePercentageBounds + function getMinimumSwapFeePercentage() external pure returns (uint256) { + return 0; + } + + /// @inheritdoc ISwapFeePercentageBounds + function getMaximumSwapFeePercentage() external pure returns (uint256) { + return 1e18; + } + + /// @inheritdoc IUnbalancedLiquidityInvariantRatioBounds + function getMinimumInvariantRatio() external pure returns (uint256) { + return 0; + } + + /// @inheritdoc IUnbalancedLiquidityInvariantRatioBounds + function getMaximumInvariantRatio() external pure returns (uint256) { + return type(uint256).max; + } +} diff --git a/pkg/pool-gyro/contracts/Gyro2CLPPoolFactory.sol b/pkg/pool-gyro/contracts/Gyro2CLPPoolFactory.sol new file mode 100644 index 000000000..503d89903 --- /dev/null +++ b/pkg/pool-gyro/contracts/Gyro2CLPPoolFactory.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { BasePoolFactory } from "@balancer-labs/v3-pool-utils/contracts/BasePoolFactory.sol"; + +import { Gyro2CLPPool } from "./Gyro2CLPPool.sol"; + +/** + * @notice Gyro 2CLP Pool factory + * @dev This is the most general factory, which allows two tokens. + */ +contract Gyro2CLPPoolFactory is BasePoolFactory { + // solhint-disable not-rely-on-time + + error SupportsOnlyTwoTokens(); + + constructor( + IVault vault, + uint32 pauseWindowDuration + ) BasePoolFactory(vault, pauseWindowDuration, type(Gyro2CLPPool).creationCode) { + // solhint-disable-previous-line no-empty-blocks + } + + /** + * @notice Deploys a new `StablePool`. + * @param name The name of the pool + * @param symbol The symbol of the pool + * @param tokens An array of descriptors for the tokens the pool will manage + * @param sqrtAlpha square root of first element in price range + * @param sqrtBeta square root of last element in price range + * @param salt The salt value that will be passed to create3 deployment + */ + function create( + string memory name, + string memory symbol, + TokenConfig[] memory tokens, + uint256 sqrtAlpha, + uint256 sqrtBeta, + PoolRoleAccounts memory roleAccounts, + uint256 swapFeePercentage, + address poolHooksContract, + bytes32 salt + ) external returns (address pool) { + if (tokens.length != 2) { + revert SupportsOnlyTwoTokens(); + } + + pool = _create( + abi.encode( + Gyro2CLPPool.GyroParams({ name: name, symbol: symbol, sqrtAlpha: sqrtAlpha, sqrtBeta: sqrtBeta }), + getVault() + ), + salt + ); + + _registerPoolWithVault( + pool, + tokens, + swapFeePercentage, + false, // not exempt from protocol fees + roleAccounts, + poolHooksContract, + getDefaultLiquidityManagement() + ); + + _registerPoolWithFactory(pool); + } +} diff --git a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol new file mode 100644 index 000000000..869802050 --- /dev/null +++ b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { Rounding } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + +import "./GyroPoolMath.sol"; + +// These functions start with an underscore, as if they were part of a contract and not a library. At some point this +// should be fixed. +// solhint-disable private-vars-leading-underscore + +/** @dev Math routines for the 2CLP. Parameters are price bounds [alpha, beta] and sqrt(alpha), sqrt(beta) are used as + * parameters. + */ +library Gyro2CLPMath { + using FixedPoint for uint256; + + error AssetBoundsExceeded(); + + // Invariant is used to calculate the virtual offsets used in swaps. + // It is also used to collect protocol swap fees by comparing its value between two times. + // So we can round always to the same direction. It is also used to initiate the BPT amount + // and, because there is a minimum BPT, we round down the invariant. + function _calculateInvariant( + uint256[] memory balances, + uint256 sqrtAlpha, + uint256 sqrtBeta, + Rounding rounding + ) internal pure returns (uint256) { + /********************************************************************************************** + // Calculate with quadratic formula + // 0 = (1-sqrt(alpha/beta)*L^2 - (y/sqrt(beta)+x*sqrt(alpha))*L - x*y) + // 0 = a*L^2 + b*L + c + // here a > 0, b < 0, and c < 0, which is a special case that works well w/o negative numbers + // taking mb = -b and mc = -c: (1/2) + // mb + (mb^2 + 4 * a * mc)^ // + // L = ------------------------------------------ // + // 2 * a // + // // + **********************************************************************************************/ + (uint256 a, uint256 mb, uint256 bSquare, uint256 mc) = _calculateQuadraticTerms( + balances, + sqrtAlpha, + sqrtBeta, + rounding + ); + return _calculateQuadratic(a, mb, bSquare, mc); + } + + /** @dev Prepares quadratic terms for input to _calculateQuadratic + * works with a special case of quadratic that works nicely w/o negative numbers + * assumes a > 0, b < 0, and c <= 0 and returns a, -b, -c + */ + function _calculateQuadraticTerms( + uint256[] memory balances, + uint256 sqrtAlpha, + uint256 sqrtBeta, + Rounding rounding + ) internal pure returns (uint256 a, uint256 mb, uint256 bSquare, uint256 mc) { + uint256[] memory newBalances = new uint256[](balances.length); + for (uint256 i = 0; i < balances.length; i++) { + newBalances[i] = balances[i] + (rounding == Rounding.ROUND_UP ? 1 : 0); + } + + { + a = FixedPoint.ONE - sqrtAlpha.divDown(sqrtBeta); + uint256 bterm0 = newBalances[1].divDown(sqrtBeta); + uint256 bterm1 = newBalances[0].mulDown(sqrtAlpha); + mb = bterm0 + bterm1; + mc = newBalances[0].mulDown(newBalances[1]); + } + // For better fixed point precision, calculate in expanded form w/ re-ordering of multiplications + // b^2 = x^2 * alpha + x*y*2*sqrt(alpha/beta) + y^2 / beta + bSquare = (newBalances[0].mulDown(newBalances[0])).mulDown(sqrtAlpha).mulDown(sqrtAlpha); + uint256 bSq2 = (newBalances[0].mulDown(newBalances[1])).mulDown(2 * FixedPoint.ONE).mulDown(sqrtAlpha).divDown( + sqrtBeta + ); + uint256 bSq3 = (newBalances[1].mulDown(newBalances[1])).divDown(sqrtBeta.mulUp(sqrtBeta)); + bSquare = bSquare + bSq2 + bSq3; + } + + /** @dev Calculates quadratic root for a special case of quadratic + * assumes a > 0, b < 0, and c <= 0, which is the case for a L^2 + b L + c = 0 + * where a = 1 - sqrt(alpha/beta) + * b = -(y/sqrt(beta) + x*sqrt(alpha)) + * c = -x*y + * The special case works nicely w/o negative numbers. + * The args use the notation "mb" to represent -b, and "mc" to represent -c + * Note that this calculates an underestimate of the solution + */ + function _calculateQuadratic( + uint256 a, + uint256 mb, + uint256 bSquare, // b^2 can be calculated separately with more precision + uint256 mc + ) internal pure returns (uint256 invariant) { + uint256 denominator = a.mulUp(2 * FixedPoint.ONE); + // order multiplications for fixed point precision + uint256 addTerm = (mc.mulDown(4 * FixedPoint.ONE)).mulDown(a); + // The minus sign in the radicand cancels out in this special case, so we add + uint256 radicand = bSquare + addTerm; + uint256 sqrResult = GyroPoolMath.sqrt(radicand, 5); + // The minus sign in the numerator cancels out in this special case + uint256 numerator = mb + sqrResult; + invariant = numerator.divDown(denominator); + } + + /** @dev Computes how many tokens can be taken out of a pool if `amountIn' are sent, given current balances + * balanceIn = existing balance of input token + * balanceOut = existing balance of requested output token + * virtualParamIn = virtual reserve offset for input token + * virtualParamOut = virtual reserve offset for output token + * Offsets are L/sqrt(beta) and L*sqrt(alpha) depending on what the `in' and `out' tokens are respectively + * Note signs are changed compared to Prop. 4 in Section 2.2.4 Trade (Swap) Exeuction to account for dy < 0 + * + * The virtualOffset argument depends on the computed invariant. We add a very small margin to ensure that + * potential small errors are not to the detriment of the pool. + * + * This is the same function as the respective function for the 3CLP, except for we allow two + * different virtual offsets for the in- and out-asset, respectively, in that other function. + * SOMEDAY: This could be made literally the same function in the pool math library. + */ + function _calcOutGivenIn( + uint256 balanceIn, + uint256 balanceOut, + uint256 amountIn, + uint256 virtualOffsetIn, + uint256 virtualOffsetOut + ) internal pure returns (uint256 amountOut) { + /********************************************************************************************** + // Described for X = `in' asset and Y = `out' asset, but equivalent for the other case // + // dX = incrX = amountIn > 0 // + // dY = incrY = amountOut < 0 // + // x = balanceIn x' = x + virtualParamX // + // y = balanceOut y' = y + virtualParamY // + // L = inv.Liq / x' * y' \ y' * dX // + // |dy| = y' - | -------------------------- | = -------------- - // + // x' = virtIn \ ( x' + dX) / x' + dX // + // y' = virtOut // + // Note that -dy > 0 is what the trader receives. // + // We exploit the fact that this formula is symmetric up to virtualOffset{X,Y}. // + // We do not use L^2, but rather x' * y', to prevent a potential accumulation of errors. // + // We add a very small safety margin to compensate for potential errors in the invariant. // + **********************************************************************************************/ + + { + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // very slightly larger than 3e-18. + uint256 virtInOver = balanceIn + virtualOffsetIn.mulUp(FixedPoint.ONE + 2); + uint256 virtOutUnder = balanceOut + virtualOffsetOut.mulDown(FixedPoint.ONE - 1); + + amountOut = virtOutUnder.mulDown(amountIn).divDown(virtInOver + amountIn); + } + + // This ensures amountOut < balanceOut. + if (!(amountOut <= balanceOut)) { + revert AssetBoundsExceeded(); + } + } + + /** @dev Computes how many tokens must be sent to a pool in order to take `amountOut`, given current balances. + * See also _calcOutGivenIn(). Adapted for negative values. */ + function _calcInGivenOut( + uint256 balanceIn, + uint256 balanceOut, + uint256 amountOut, + uint256 virtualOffsetIn, + uint256 virtualOffsetOut + ) internal pure returns (uint256 amountIn) { + /********************************************************************************************** + // dX = incrX = amountIn > 0 // + // dY = incrY = amountOut < 0 // + // x = balanceIn x' = x + virtualParamX // + // y = balanceOut y' = y + virtualParamY // + // x = balanceIn // + // L = inv.Liq / x' * y' \ x' * dy // + // dx = | -------------------------- | - x' = - ----------- // + // x' = virtIn \ y' + dy / y' + dy // + // y' = virtOut // + // Note that dy < 0 < dx. // + // We exploit the fact that this formula is symmetric up to virtualOffset{X,Y}. // + // We do not use L^2, but rather x' * y', to prevent a potential accumulation of errors. // + // We add a very small safety margin to compensate for potential errors in the invariant. // + **********************************************************************************************/ + if (!(amountOut <= balanceOut)) { + revert AssetBoundsExceeded(); + } + + { + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // very slightly larger than 3e-18. + uint256 virtInOver = balanceIn + virtualOffsetIn.mulUp(FixedPoint.ONE + 2); + uint256 virtOutUnder = balanceOut + virtualOffsetOut.mulDown(FixedPoint.ONE - 1); + + amountIn = virtInOver.mulUp(amountOut).divUp(virtOutUnder - amountOut); + } + } + + /** @dev Calculate virtual offset a for reserves x, as in (x+a)*(y+b)=L^2 + */ + function _calculateVirtualParameter0(uint256 invariant, uint256 _sqrtBeta) internal pure returns (uint256) { + return invariant.divDown(_sqrtBeta); + } + + /** @dev Calculate virtual offset b for reserves y, as in (x+a)*(y+b)=L^2 + */ + function _calculateVirtualParameter1(uint256 invariant, uint256 _sqrtAlpha) internal pure returns (uint256) { + return invariant.mulDown(_sqrtAlpha); + } + + /** @dev Calculates the spot price of token A in units of token B. + * + * The spot price is bounded by pool parameters due to virtual reserves. Aside from being instantaneously + * manipulable within a transaction, it may also not be accurate if the true price is outside of these bounds. + */ + function _calcSpotPriceAinB( + uint256 balanceA, + uint256 virtualParameterA, + uint256 balanceB, + uint256 virtualParameterB + ) internal pure returns (uint256) { + return (balanceB + virtualParameterB).divUp((balanceA + virtualParameterA)); + } +} diff --git a/pkg/pool-gyro/contracts/lib/GyroPoolMath.sol b/pkg/pool-gyro/contracts/lib/GyroPoolMath.sol new file mode 100644 index 000000000..76cda8ec2 --- /dev/null +++ b/pkg/pool-gyro/contracts/lib/GyroPoolMath.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: LicenseRef-Gyro-1.0 +// for information on licensing please see the README in the GitHub repository +// . + +pragma solidity ^0.8.24; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + +library GyroPoolMath { + using FixedPoint for uint256; + + uint256 private constant _SQRT_1E_NEG_1 = 316227766016837933; + uint256 private constant _SQRT_1E_NEG_3 = 31622776601683793; + uint256 private constant _SQRT_1E_NEG_5 = 3162277660168379; + uint256 private constant _SQRT_1E_NEG_7 = 316227766016837; + uint256 private constant _SQRT_1E_NEG_9 = 31622776601683; + uint256 private constant _SQRT_1E_NEG_11 = 3162277660168; + uint256 private constant _SQRT_1E_NEG_13 = 316227766016; + uint256 private constant _SQRT_1E_NEG_15 = 31622776601; + uint256 private constant _SQRT_1E_NEG_17 = 3162277660; + + /** @dev Implements square root algorithm using Newton's method and a first-guess optimisation **/ + function sqrt(uint256 input, uint256 tolerance) internal pure returns (uint256) { + if (input == 0) { + return 0; + } + + uint256 guess = _makeInitialGuess(input); + + // 7 iterations + guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; + guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; + guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; + guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; + guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; + guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; + guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; + + // Check in some epsilon range + // Check square is more or less correct + uint256 guessSquared = guess.mulDown(guess); + require( + guessSquared <= input + guess.mulUp(tolerance) && guessSquared >= input - guess.mulUp(tolerance), + "_sqrt FAILED" + ); + + return guess; + } + + function _makeInitialGuess(uint256 input) private pure returns (uint256) { + if (input >= FixedPoint.ONE) { + return (1 << (_intLog2Halved(input / FixedPoint.ONE))) * FixedPoint.ONE; + } else { + if (input <= 10) { + return _SQRT_1E_NEG_17; + } + if (input <= 1e2) { + return 1e10; + } + if (input <= 1e3) { + return _SQRT_1E_NEG_15; + } + if (input <= 1e4) { + return 1e11; + } + if (input <= 1e5) { + return _SQRT_1E_NEG_13; + } + if (input <= 1e6) { + return 1e12; + } + if (input <= 1e7) { + return _SQRT_1E_NEG_11; + } + if (input <= 1e8) { + return 1e13; + } + if (input <= 1e9) { + return _SQRT_1E_NEG_9; + } + if (input <= 1e10) { + return 1e14; + } + if (input <= 1e11) { + return _SQRT_1E_NEG_7; + } + if (input <= 1e12) { + return 1e15; + } + if (input <= 1e13) { + return _SQRT_1E_NEG_5; + } + if (input <= 1e14) { + return 1e16; + } + if (input <= 1e15) { + return _SQRT_1E_NEG_3; + } + if (input <= 1e16) { + return 1e17; + } + if (input <= 1e17) { + return _SQRT_1E_NEG_1; + } + return input; + } + } + + function _intLog2Halved(uint256 x) private pure returns (uint256 n) { + if (x >= 1 << 128) { + x >>= 128; + n += 64; + } + if (x >= 1 << 64) { + x >>= 64; + n += 32; + } + if (x >= 1 << 32) { + x >>= 32; + n += 16; + } + if (x >= 1 << 16) { + x >>= 16; + n += 8; + } + if (x >= 1 << 8) { + x >>= 8; + n += 4; + } + if (x >= 1 << 4) { + x >>= 4; + n += 2; + } + if (x >= 1 << 2) { + x >>= 2; + n += 1; + } + } +} diff --git a/pkg/pool-gyro/coverage.sh b/pkg/pool-gyro/coverage.sh new file mode 100755 index 000000000..a2fa0a8e8 --- /dev/null +++ b/pkg/pool-gyro/coverage.sh @@ -0,0 +1,124 @@ +#!/bin/bash + +set -e # exit on error + +# Functions + +function forge_coverage() { + echo 'Running Forge coverage' + + # reduces the amount of tests in fuzzing, so coverage runs faster + export FOUNDRY_PROFILE=coverage + export CURRENT_PACKAGE=$(basename "$PWD") + + # generates lcov.info + forge coverage --report lcov + + # Initialize variables + current_file="" + lines_found=0 + lines_hit=0 + + sed "s/\/.*$CURRENT_PACKAGE.//g" lcov.info > lcov-forge.info + sed -i -e "s/\.\.contracts\//contracts\//g" lcov-forge.info +} + +function hardhat_coverage() { + echo 'Running Hardhat coverage' + # generates coverage/lcov.info + COVERAGE=true yarn hardhat coverage + + # Foundry uses relative paths but Hardhat uses absolute paths. + # Convert absolute paths to relative paths for consistency. + sed -i -e "s/\/.*$CURRENT_PACKAGE.//g" coverage/lcov.info + mv coverage/lcov.info lcov-hardhat.info +} + +function merge() { + echo 'Merging coverage files...' + + if [[ "$1" == 'forge' ]]; then + lcov \ + --rc lcov_branch_coverage=1 \ + --rc derive_function_end_line=0 \ + --add-tracefile lcov-forge.info \ + --output-file lcov-merged.info + elif [[ "$1" == 'hardhat' ]]; then + lcov \ + --rc lcov_branch_coverage=1 \ + --rc derive_function_end_line=0 \ + --add-tracefile lcov-forge.info \ + --output-file lcov-merged.info + elif [[ "$1" == 'all' ]]; then + lcov \ + --rc lcov_branch_coverage=1 \ + --rc derive_function_end_line=0 \ + --add-tracefile lcov-forge.info \ + --add-tracefile lcov-hardhat.info \ + --output-file lcov-merged.info + fi +} + +function filter_and_display() { + echo 'Filtering report...' + + if [[ $CURRENT_PACKAGE == "vault" ]]; then + # Filter out node_modules, test, and mock files + lcov \ + --rc lcov_branch_coverage=1 \ + --ignore-errors unused \ + --rc derive_function_end_line=0 \ + --remove lcov-merged.info \ + "*node_modules*" "*test*" "*Mock*" \ + --output-file lcov-filtered.info + else + # Filter out node_modules, test, mock files and vault contracts + lcov \ + --rc lcov_branch_coverage=1 \ + --ignore-errors unused \ + --rc derive_function_end_line=0 \ + --remove lcov-merged.info \ + "*node_modules*" "*test*" "*Mock*" "*/vault/*" \ + --output-file lcov-filtered.info + fi + + echo 'Generating summary...' + + # Generate summary + lcov \ + --rc lcov_branch_coverage=1 \ + --rc derive_function_end_line=0 \ + --list lcov-filtered.info + + echo 'Display!' + + # Open more granular breakdown in browser + rm -rf coverage-genhtml/ + genhtml \ + --rc lcov_branch_coverage=1 \ + --rc derive_function_end_line=0 \ + --output-directory coverage-genhtml \ + lcov-filtered.info + open coverage-genhtml/index.html +} + +# Script + +if [[ "$1" == 'forge' ]]; then + forge_coverage +elif [[ "$1" == 'hardhat' ]]; then + hardhat_coverage +elif [[ "$1" == 'all' ]]; then + forge_coverage + hardhat_coverage +else + echo 'Usage: ./coverage.sh [forge | hardhat | all]' + exit 1 +fi + +merge "$1" + +filter_and_display + +# Delete temp files +rm -rf lcov-*.info lcov-*.info-e coverage/ lcov.info coverage.json diff --git a/pkg/pool-gyro/foundry.toml b/pkg/pool-gyro/foundry.toml new file mode 100755 index 000000000..8b4179b43 --- /dev/null +++ b/pkg/pool-gyro/foundry.toml @@ -0,0 +1,50 @@ +[profile.default] +src = 'contracts' +out = 'forge-artifacts' +libs = ['node_modules'] +test = 'test/foundry' +cache_path = 'forge-cache' +allow_paths = ['../', '../../node_modules/'] +ffi = true +fs_permissions = [ + { access = "read", path = "./artifacts/" }, + { access = "read-write", path = "./.forge-snapshots/"}, +] +remappings = [ + 'vault/=../vault/', + 'pool-weighted/=../pool-weighted/', + 'solidity-utils/=../solidity-utils/', + 'ds-test/=../../node_modules/forge-std/lib/ds-test/src/', + 'forge-std/=../../node_modules/forge-std/src/', + '@openzeppelin/=../../node_modules/@openzeppelin/', + 'permit2/=../../node_modules/permit2/', + '@balancer-labs/=../../node_modules/@balancer-labs/', + 'forge-gas-snapshot/=../../node_modules/forge-gas-snapshot/src/' +] +optimizer = true +optimizer_runs = 999 +solc_version = '0.8.26' +auto_detect_solc = false +evm_version = 'cancun' +ignored_error_codes = [2394, 5574, 3860] # Transient storage, code size + +[fuzz] +runs = 10000 +max_test_rejects = 60000 + +[profile.forkfuzz.fuzz] +runs = 1000 +max_test_rejects = 60000 + +[profile.coverage.fuzz] +runs = 100 +max_test_rejects = 60000 + +[profile.intense.fuzz] +verbosity = 3 +runs = 100000 +max_test_rejects = 600000 + +[rpc_endpoints] + mainnet = "${MAINNET_RPC_URL}" + sepolia = "${SEPOLIA_RPC_URL}" diff --git a/pkg/pool-gyro/hardhat.config.ts b/pkg/pool-gyro/hardhat.config.ts new file mode 100644 index 000000000..f42e8cc79 --- /dev/null +++ b/pkg/pool-gyro/hardhat.config.ts @@ -0,0 +1,20 @@ +import '@nomicfoundation/hardhat-ethers'; +import '@nomicfoundation/hardhat-toolbox'; +import '@typechain/hardhat'; + +import 'hardhat-ignore-warnings'; +import 'hardhat-gas-reporter'; + +import { hardhatBaseConfig } from '@balancer-labs/v3-common'; + +export default { + networks: { + hardhat: { + allowUnlimitedContractSize: true, + }, + }, + solidity: { + compilers: hardhatBaseConfig.compilers, + }, + warnings: hardhatBaseConfig.warnings, +}; diff --git a/pkg/pool-gyro/package.json b/pkg/pool-gyro/package.json new file mode 100644 index 000000000..2d314d688 --- /dev/null +++ b/pkg/pool-gyro/package.json @@ -0,0 +1,60 @@ +{ + "name": "@balancer-labs/v3-pool-gyro", + "version": "0.1.0", + "description": "Balancer V3 Gyro Pools", + "license": "GPL-3.0-only", + "homepage": "https://github.com/balancer-labs/balancer-v3-monorepo/tree/master/pkg/pool-gyro#readme", + "repository": { + "type": "git", + "url": "https://github.com/balancer-labs/balancer-v3-monorepo.git", + "directory": "pkg/pool-gyro" + }, + "bugs": { + "url": "https://github.com/balancer-labs/balancer-v3-monorepo/issues" + }, + "files": [ + "contracts/**/*.sol", + "!contracts/test/**/*.sol" + ], + "scripts": { + "build": "yarn compile && rm -rf artifacts/build-info", + "compile": "hardhat compile", + "compile:watch": "nodemon --ext sol --exec yarn compile", + "lint": "yarn lint:solidity && yarn lint:typescript", + "lint:solidity": "npx prettier --check --plugin=prettier-plugin-solidity 'contracts/**/*.sol' ''test/**/*.sol'' && npx solhint 'contracts/**/*.sol'", + "lint:typescript": "NODE_NO_WARNINGS=1 eslint --ext .ts --ignore-path ../../.eslintignore --max-warnings 0", + "prettier": "npx prettier --write --plugin=prettier-plugin-solidity 'contracts/**/*.sol' 'test/**/*.sol'", + "test": "yarn test:hardhat && yarn test:forge", + "test:hardhat": "hardhat test", + "test:forge": "forge test --ffi -vvv", + "test:stress": "FOUNDRY_PROFILE=intense forge test --ffi -vvv", + "coverage": "coverage.sh", + "gas": "REPORT_GAS=true hardhat test", + "test:watch": "nodemon --ext js,ts --watch test --watch lib --exec 'clear && yarn test --no-compile'", + "slither": "yarn compile && bash -c 'source ../../slither/bin/activate && slither --compile-force-framework hardhat --ignore-compile --config-file ../../.slither.config.json'", + "slither:triage": "yarn compile && bash -c 'source ../../slither/bin/activate && slither --compile-force-framework hardhat --ignore-compile --config-file ../../.slither.config.json --triage-mode'" + }, + "dependencies": { + "@balancer-labs/v3-interfaces": "workspace:*" + }, + "devDependencies": { + "@balancer-labs/solidity-toolbox": "workspace:*", + "@balancer-labs/v3-solidity-utils": "workspace:*", + "@typescript-eslint/eslint-plugin": "^5.41.0", + "@typescript-eslint/parser": "^5.41.0", + "decimal.js": "^10.4.2", + "eslint": "^8.26.0", + "eslint-plugin-mocha-no-only": "^1.1.1", + "eslint-plugin-prettier": "^4.2.1", + "hardhat": "^2.14.0", + "lodash.frompairs": "^4.0.1", + "lodash.pick": "^4.4.0", + "lodash.range": "^3.2.0", + "lodash.times": "^4.3.2", + "lodash.zip": "^4.2.0", + "mathjs": "^11.8.0", + "mocha": "^10.1.0", + "nodemon": "^2.0.20", + "solhint": "^3.4.1" + } +} diff --git a/pkg/pool-gyro/test/foundry/ComputeBalance.t.sol b/pkg/pool-gyro/test/foundry/ComputeBalance.t.sol new file mode 100644 index 000000000..2221ed946 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/ComputeBalance.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { Rounding } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + +import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol"; + +import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; + +contract ComputeBalanceTest is BaseVaultTest { + using FixedPoint for uint256; + + Gyro2CLPPool private _gyroPool; + uint256 private _sqrtAlpha = 997496867163000167; // alpha (lower price rate) = 0.995 + uint256 private _sqrtBeta = 1002496882788171068; // beta (upper price rate) = 1.005 + + function setUp() public virtual override { + BaseVaultTest.setUp(); + + _gyroPool = new Gyro2CLPPool( + Gyro2CLPPool.GyroParams({ name: "GyroPool", symbol: "GRP", sqrtAlpha: _sqrtAlpha, sqrtBeta: _sqrtBeta }), + vault + ); + vm.label(address(_gyroPool), "GyroPool"); + } + + function testComputeNewXBalance__Fuzz(uint256 balanceX, uint256 balanceY, uint256 deltaX) public { + balanceX = bound(balanceX, 1e16, 1e27); + // Price range is [alpha,beta], so balanceY needs to be between alpha*balanceX and beta*balanceX + balanceY = bound( + balanceY, + balanceX.mulDown(_sqrtAlpha).mulDown(_sqrtAlpha), + balanceX.mulDown(_sqrtBeta).mulDown(_sqrtBeta) + ); + uint256[] memory balances = new uint256[](2); + balances[0] = balanceX; + balances[1] = balanceY; + uint256 oldInvariant = _gyroPool.computeInvariant(balances, Rounding.ROUND_DOWN); + + deltaX = bound(deltaX, 1e16, 1e30); + balances[0] = balances[0] + deltaX; + uint256 newInvariant = _gyroPool.computeInvariant(balances, Rounding.ROUND_DOWN); + balances[0] = balances[0] - deltaX; + + uint256 invariantRatio = newInvariant.divDown(oldInvariant); + uint256 newXBalance = _gyroPool.computeBalance(balances, 0, invariantRatio); + + // 0.000000000002% error + assertApproxEqRel(newXBalance, balanceX + deltaX, 2e4); + } + + function testComputeNewYBalance__Fuzz(uint256 balanceX, uint256 balanceY, uint256 deltaY) public { + balanceX = bound(balanceX, 1e16, 1e27); + // Price range is [alpha,beta], so balanceY needs to be between alpha*balanceX and beta*balanceX + balanceY = bound( + balanceY, + balanceX.mulDown(_sqrtAlpha).mulDown(_sqrtAlpha), + balanceX.mulDown(_sqrtBeta).mulDown(_sqrtBeta) + ); + uint256[] memory balances = new uint256[](2); + balances[0] = balanceX; + balances[1] = balanceY; + uint256 oldInvariant = _gyroPool.computeInvariant(balances, Rounding.ROUND_DOWN); + + deltaY = bound(deltaY, 1e16, 1e30); + balances[1] = balances[1] + deltaY; + uint256 newInvariant = _gyroPool.computeInvariant(balances, Rounding.ROUND_DOWN); + balances[1] = balances[1] - deltaY; + + uint256 invariantRatio = newInvariant.divDown(oldInvariant); + uint256 newYBalance = _gyroPool.computeBalance(balances, 1, invariantRatio); + + // 0.000000000002% error + assertApproxEqRel(newYBalance, balanceY + deltaY, 2e4); + } +} diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol new file mode 100644 index 000000000..fb9c11c12 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { PoolRoleAccounts } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; + +import { Gyro2CLPPoolFactory } from "../../contracts/Gyro2CLPPoolFactory.sol"; +import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; + +import { LiquidityApproximationTest } from "@balancer-labs/v3-vault/test/foundry/LiquidityApproximation.t.sol"; +import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; + +contract LiquidityApproximationGyroTest is LiquidityApproximationTest { + using CastingHelpers for address[]; + + uint256 poolCreationNonce; + + uint256 private _sqrtAlpha = 997496867163000167; // alpha (lower price rate) = 0.995 + uint256 private _sqrtBeta = 1002496882788171068; // beta (upper price rate) = 1.005 + + function setUp() public virtual override { + LiquidityApproximationTest.setUp(); + } + + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + Gyro2CLPPoolFactory factory = new Gyro2CLPPoolFactory(IVault(address(vault)), 365 days); + + PoolRoleAccounts memory roleAccounts; + + Gyro2CLPPool newPool = Gyro2CLPPool( + factory.create( + "Gyro 2CLP Pool", + "GRP", + vault.buildTokenConfig(tokens.asIERC20()), + _sqrtAlpha, + _sqrtBeta, + roleAccounts, + 0, + address(0), + ZERO_BYTES32 + ) + ); + vm.label(address(newPool), label); + return address(newPool); + } +} diff --git a/pkg/pool-gyro/tsconfig.json b/pkg/pool-gyro/tsconfig.json new file mode 100644 index 000000000..b4e69ae1f --- /dev/null +++ b/pkg/pool-gyro/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + } +} diff --git a/yarn.lock b/yarn.lock index 710bdfc53..b3ad0b5c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -193,6 +193,32 @@ __metadata: languageName: unknown linkType: soft +"@balancer-labs/v3-pool-gyro@workspace:pkg/pool-gyro": + version: 0.0.0-use.local + resolution: "@balancer-labs/v3-pool-gyro@workspace:pkg/pool-gyro" + dependencies: + "@balancer-labs/solidity-toolbox": "workspace:*" + "@balancer-labs/v3-interfaces": "workspace:*" + "@balancer-labs/v3-solidity-utils": "workspace:*" + "@typescript-eslint/eslint-plugin": "npm:^5.41.0" + "@typescript-eslint/parser": "npm:^5.41.0" + decimal.js: "npm:^10.4.2" + eslint: "npm:^8.26.0" + eslint-plugin-mocha-no-only: "npm:^1.1.1" + eslint-plugin-prettier: "npm:^4.2.1" + hardhat: "npm:^2.14.0" + lodash.frompairs: "npm:^4.0.1" + lodash.pick: "npm:^4.4.0" + lodash.range: "npm:^3.2.0" + lodash.times: "npm:^4.3.2" + lodash.zip: "npm:^4.2.0" + mathjs: "npm:^11.8.0" + mocha: "npm:^10.1.0" + nodemon: "npm:^2.0.20" + solhint: "npm:^3.4.1" + languageName: unknown + linkType: soft + "@balancer-labs/v3-pool-hooks@workspace:pkg/pool-hooks": version: 0.0.0-use.local resolution: "@balancer-labs/v3-pool-hooks@workspace:pkg/pool-hooks" From ddbbe5b0cc54956958a8ec0c04c42e5aee50f554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 9 Oct 2024 19:38:54 -0300 Subject: [PATCH 02/52] Gyro Liquidity Approx Tests --- pkg/pool-gyro/contracts/Gyro2CLPPool.sol | 2 +- pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol | 22 +++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/pkg/pool-gyro/contracts/Gyro2CLPPool.sol b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol index 798a8d2bc..b275154b9 100644 --- a/pkg/pool-gyro/contracts/Gyro2CLPPool.sol +++ b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol @@ -84,7 +84,7 @@ contract Gyro2CLPPool is IBasePool, BalancerPoolToken { balancesLiveScaled18, sqrtParams[0], sqrtParams[1], - Rounding.ROUND_DOWN + Rounding.ROUND_UP ); // New invariant invariant = invariant.mulUp(invariantRatio); diff --git a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol index 869802050..a5b5d373e 100644 --- a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol @@ -65,20 +65,28 @@ library Gyro2CLPMath { newBalances[i] = balances[i] + (rounding == Rounding.ROUND_UP ? 1 : 0); } + function(uint256, uint256) pure returns (uint256) _divUpOrDown = rounding == Rounding.ROUND_DOWN + ? FixedPoint.divDown + : FixedPoint.divUp; + function(uint256, uint256) pure returns (uint256) _mulUpOrDown = rounding == Rounding.ROUND_DOWN + ? FixedPoint.mulDown + : FixedPoint.mulUp; + { - a = FixedPoint.ONE - sqrtAlpha.divDown(sqrtBeta); - uint256 bterm0 = newBalances[1].divDown(sqrtBeta); - uint256 bterm1 = newBalances[0].mulDown(sqrtAlpha); + a = FixedPoint.ONE - _divUpOrDown(sqrtAlpha, sqrtBeta); + uint256 bterm0 = _divUpOrDown(newBalances[1], sqrtBeta); + uint256 bterm1 = _mulUpOrDown(newBalances[0], sqrtAlpha); mb = bterm0 + bterm1; - mc = newBalances[0].mulDown(newBalances[1]); + mc = _mulUpOrDown(newBalances[0], newBalances[1]); } // For better fixed point precision, calculate in expanded form w/ re-ordering of multiplications // b^2 = x^2 * alpha + x*y*2*sqrt(alpha/beta) + y^2 / beta - bSquare = (newBalances[0].mulDown(newBalances[0])).mulDown(sqrtAlpha).mulDown(sqrtAlpha); - uint256 bSq2 = (newBalances[0].mulDown(newBalances[1])).mulDown(2 * FixedPoint.ONE).mulDown(sqrtAlpha).divDown( + bSquare = _mulUpOrDown(_mulUpOrDown(newBalances[0], newBalances[0]), _mulUpOrDown(sqrtAlpha, sqrtAlpha)); + uint256 bSq2 = _divUpOrDown( + 2 * _mulUpOrDown(_mulUpOrDown(newBalances[0], newBalances[1]), sqrtAlpha), sqrtBeta ); - uint256 bSq3 = (newBalances[1].mulDown(newBalances[1])).divDown(sqrtBeta.mulUp(sqrtBeta)); + uint256 bSq3 = _divUpOrDown(_mulUpOrDown(newBalances[1], newBalances[1]), sqrtBeta.mulUp(sqrtBeta)); bSquare = bSquare + bSq2 + bSq3; } From 7a20c2bbfacb5cda48dcb5d462def2c73c997f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 10 Oct 2024 14:03:25 -0300 Subject: [PATCH 03/52] E2e tests --- pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol | 20 +-- pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol | 75 ++++++++++++ pkg/pool-gyro/test/foundry/E2eSwap.t.sol | 114 ++++++++++++++++++ .../test/foundry/E2eSwapRateProvider.t.sol | 112 +++++++++++++++++ 4 files changed, 307 insertions(+), 14 deletions(-) create mode 100644 pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol create mode 100644 pkg/pool-gyro/test/foundry/E2eSwap.t.sol create mode 100644 pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol diff --git a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol index a5b5d373e..a532143d4 100644 --- a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol @@ -60,11 +60,6 @@ library Gyro2CLPMath { uint256 sqrtBeta, Rounding rounding ) internal pure returns (uint256 a, uint256 mb, uint256 bSquare, uint256 mc) { - uint256[] memory newBalances = new uint256[](balances.length); - for (uint256 i = 0; i < balances.length; i++) { - newBalances[i] = balances[i] + (rounding == Rounding.ROUND_UP ? 1 : 0); - } - function(uint256, uint256) pure returns (uint256) _divUpOrDown = rounding == Rounding.ROUND_DOWN ? FixedPoint.divDown : FixedPoint.divUp; @@ -74,19 +69,16 @@ library Gyro2CLPMath { { a = FixedPoint.ONE - _divUpOrDown(sqrtAlpha, sqrtBeta); - uint256 bterm0 = _divUpOrDown(newBalances[1], sqrtBeta); - uint256 bterm1 = _mulUpOrDown(newBalances[0], sqrtAlpha); + uint256 bterm0 = _divUpOrDown(balances[1], sqrtBeta); + uint256 bterm1 = _mulUpOrDown(balances[0], sqrtAlpha); mb = bterm0 + bterm1; - mc = _mulUpOrDown(newBalances[0], newBalances[1]); + mc = _mulUpOrDown(balances[0], balances[1]); } // For better fixed point precision, calculate in expanded form w/ re-ordering of multiplications // b^2 = x^2 * alpha + x*y*2*sqrt(alpha/beta) + y^2 / beta - bSquare = _mulUpOrDown(_mulUpOrDown(newBalances[0], newBalances[0]), _mulUpOrDown(sqrtAlpha, sqrtAlpha)); - uint256 bSq2 = _divUpOrDown( - 2 * _mulUpOrDown(_mulUpOrDown(newBalances[0], newBalances[1]), sqrtAlpha), - sqrtBeta - ); - uint256 bSq3 = _divUpOrDown(_mulUpOrDown(newBalances[1], newBalances[1]), sqrtBeta.mulUp(sqrtBeta)); + bSquare = _mulUpOrDown(_mulUpOrDown(balances[0], balances[0]), _mulUpOrDown(sqrtAlpha, sqrtAlpha)); + uint256 bSq2 = _divUpOrDown(2 * _mulUpOrDown(_mulUpOrDown(balances[0], balances[1]), sqrtAlpha), sqrtBeta); + uint256 bSq3 = _divUpOrDown(_mulUpOrDown(balances[1], balances[1]), sqrtBeta.mulUp(sqrtBeta)); bSquare = bSquare + bSq2 + bSq3; } diff --git a/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol b/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol new file mode 100644 index 000000000..9356f2edf --- /dev/null +++ b/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { PoolRoleAccounts } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; +import { ERC20TestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/ERC20TestToken.sol"; + +import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; +import { E2eBatchSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eBatchSwap.t.sol"; + +import { Gyro2CLPPoolFactory } from "../../contracts/Gyro2CLPPoolFactory.sol"; +import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; + +contract E2eBatchSwapGyro2CLPTest is E2eBatchSwapTest { + using CastingHelpers for address[]; + + uint256 poolCreationNonce; + + uint256 private _sqrtAlpha = 997496867163000167; // alpha (lower price rate) = 0.995 + uint256 private _sqrtBeta = 1002496882788171068; // beta (upper price rate) = 1.005 + + function _setUpVariables() internal override { + tokenA = dai; + tokenB = usdc; + tokenC = ERC20TestToken(address(weth)); + tokenD = wsteth; + sender = lp; + poolCreator = lp; + + // If there are swap fees, the amountCalculated may be lower than MIN_TRADE_AMOUNT. So, multiplying + // MIN_TRADE_AMOUNT by 10 creates a margin. + minSwapAmountTokenA = 10 * PRODUCTION_MIN_TRADE_AMOUNT; + minSwapAmountTokenD = 10 * PRODUCTION_MIN_TRADE_AMOUNT; + + // Divide init amount by 10 to make sure weighted math ratios are respected (Cannot trade more than 30% of pool + // balance). + maxSwapAmountTokenA = poolInitAmount / 10; + maxSwapAmountTokenD = poolInitAmount / 10; + } + + /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eBatchSwapTest tests. + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + Gyro2CLPPoolFactory factory = new Gyro2CLPPoolFactory(IVault(address(vault)), 365 days); + + PoolRoleAccounts memory roleAccounts; + + Gyro2CLPPool newPool = Gyro2CLPPool( + factory.create( + "Gyro 2CLP Pool", + "GRP", + vault.buildTokenConfig(tokens.asIERC20()), + _sqrtAlpha, + _sqrtBeta, + roleAccounts, + 0, + address(0), + ZERO_BYTES32 + ) + ); + vm.label(address(newPool), label); + + // Cannot set the pool creator directly on a standard Balancer stable pool factory. + vault.manualSetPoolCreator(address(newPool), lp); + + ProtocolFeeControllerMock feeController = ProtocolFeeControllerMock(address(vault.getProtocolFeeController())); + feeController.manualSetPoolCreator(address(newPool), lp); + + return address(newPool); + } +} diff --git a/pkg/pool-gyro/test/foundry/E2eSwap.t.sol b/pkg/pool-gyro/test/foundry/E2eSwap.t.sol new file mode 100644 index 000000000..6d0b12fb8 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/E2eSwap.t.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + +import { PoolHooksMock } from "@balancer-labs/v3-vault/contracts/test/PoolHooksMock.sol"; +import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; +import { E2eSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwap.t.sol"; + +import { Gyro2CLPPoolFactory } from "../../contracts/Gyro2CLPPoolFactory.sol"; +import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; + +contract E2eSwapGyro2CLPTest is E2eSwapTest { + using CastingHelpers for address[]; + using FixedPoint for uint256; + + uint256 poolCreationNonce; + + uint256 private _sqrtAlpha = 997496867163000167; // alpha (lower price rate) = 0.995 + uint256 private _sqrtBeta = 1002496882788171068; // beta (upper price rate) = 1.005 + + function setUp() public override { + E2eSwapTest.setUp(); + } + + function setUpVariables() internal override { + sender = lp; + poolCreator = lp; + + // 0.0001% min swap fee. + minPoolSwapFeePercentage = 1e12; + // 10% max swap fee. + maxPoolSwapFeePercentage = 10e16; + } + + function calculateMinAndMaxSwapAmounts() internal virtual override { + uint256 rateTokenA = getRate(tokenA); + uint256 rateTokenB = getRate(tokenB); + + // The vault does not allow trade amounts (amountGivenScaled18 or amountCalculatedScaled18) to be less than + // MIN_TRADE_AMOUNT. For "linear pools" (PoolMock), amountGivenScaled18 and amountCalculatedScaled18 are + // the same. So, minAmountGivenScaled18 > MIN_TRADE_AMOUNT. To derive the formula below, note that + // `amountGivenRaw = amountGivenScaled18/(rateToken * scalingFactor)`. There's an adjustment for stable math + // in the following steps. + uint256 tokenAMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenA).mulUp(10 ** decimalsTokenA); + uint256 tokenBMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenB).mulUp(10 ** decimalsTokenB); + + // Also, since we undo the operation (reverse swap with the output of the first swap), amountCalculatedRaw + // cannot be 0. Considering that amountCalculated is tokenB, and amountGiven is tokenA: + // 1) amountCalculatedRaw > 0 + // 2) amountCalculatedRaw = amountCalculatedScaled18 * 10^(decimalsB) / (rateB * 10^18) + // 3) amountCalculatedScaled18 = amountGivenScaled18 // Linear math, there's a factor to stable math + // 4) amountGivenScaled18 = amountGivenRaw * rateA * 10^18 / 10^(decimalsA) + // Using the four formulas above, we determine that: + // amountCalculatedRaw > rateB * 10^(decimalsA) / (rateA * 10^(decimalsB)) + uint256 tokenACalculatedNotZero = (rateTokenB * (10 ** decimalsTokenA)) / (rateTokenA * (10 ** decimalsTokenB)); + uint256 tokenBCalculatedNotZero = (rateTokenA * (10 ** decimalsTokenB)) / (rateTokenB * (10 ** decimalsTokenA)); + + // Use the larger of the two values above to calculate the minSwapAmount. Also, multiply by 10 to account for + // swap fees and compensate for rate rounding issues. + uint256 mathFactor = 10; + minSwapAmountTokenA = ( + tokenAMinTradeAmount > tokenACalculatedNotZero + ? mathFactor * tokenAMinTradeAmount + : mathFactor * tokenACalculatedNotZero + ); + minSwapAmountTokenB = ( + tokenBMinTradeAmount > tokenBCalculatedNotZero + ? mathFactor * tokenBMinTradeAmount + : mathFactor * tokenBCalculatedNotZero + ); + + // 50% of pool init amount to make sure LP has enough tokens to pay for the swap in case of EXACT_OUT. + maxSwapAmountTokenA = poolInitAmountTokenA.mulDown(50e16); + maxSwapAmountTokenB = poolInitAmountTokenB.mulDown(50e16); + } + + /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eSwapTest tests. + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + Gyro2CLPPoolFactory factory = new Gyro2CLPPoolFactory(IVault(address(vault)), 365 days); + + PoolRoleAccounts memory roleAccounts; + + Gyro2CLPPool newPool = Gyro2CLPPool( + factory.create( + "Gyro 2CLP Pool", + "GRP", + vault.buildTokenConfig(tokens.asIERC20()), + _sqrtAlpha, + _sqrtBeta, + roleAccounts, + 0, + address(0), + ZERO_BYTES32 + ) + ); + vm.label(address(newPool), label); + + // Cannot set the pool creator directly on a standard Balancer stable pool factory. + vault.manualSetPoolCreator(address(newPool), lp); + + ProtocolFeeControllerMock feeController = ProtocolFeeControllerMock(address(vault.getProtocolFeeController())); + feeController.manualSetPoolCreator(address(newPool), lp); + + return address(newPool); + } +} diff --git a/pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol new file mode 100644 index 000000000..1d8b15054 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + +import { PoolHooksMock } from "@balancer-labs/v3-vault/contracts/test/PoolHooksMock.sol"; +import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; +import { RateProviderMock } from "@balancer-labs/v3-vault/contracts/test/RateProviderMock.sol"; +import { E2eSwapRateProviderTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwapRateProvider.t.sol"; +import { VaultContractsDeployer } from "@balancer-labs/v3-vault/test/foundry/utils/VaultContractsDeployer.sol"; + +import { Gyro2CLPPoolFactory } from "../../contracts/Gyro2CLPPoolFactory.sol"; +import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; + +contract E2eSwapRateProviderGyro2CLPTest is VaultContractsDeployer, E2eSwapRateProviderTest { + using CastingHelpers for address[]; + using FixedPoint for uint256; + + uint256 poolCreationNonce; + + uint256 private _sqrtAlpha = 997496867163000167; // alpha (lower price rate) = 0.995 + uint256 private _sqrtBeta = 1002496882788171068; // beta (upper price rate) = 1.005 + + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + rateProviderTokenA = deployRateProviderMock(); + rateProviderTokenB = deployRateProviderMock(); + // Mock rates, so all tests that keep the rate constant use a rate different than 1. + rateProviderTokenA.mockRate(5.2453235e18); + rateProviderTokenB.mockRate(0.4362784e18); + + IRateProvider[] memory rateProviders = new IRateProvider[](2); + rateProviders[tokenAIdx] = IRateProvider(address(rateProviderTokenA)); + rateProviders[tokenBIdx] = IRateProvider(address(rateProviderTokenB)); + + Gyro2CLPPoolFactory factory = new Gyro2CLPPoolFactory(IVault(address(vault)), 365 days); + + PoolRoleAccounts memory roleAccounts; + + Gyro2CLPPool newPool = Gyro2CLPPool( + factory.create( + "Gyro 2CLP Pool", + "GRP", + vault.buildTokenConfig(tokens.asIERC20(), rateProviders), + _sqrtAlpha, + _sqrtBeta, + roleAccounts, + 0, + address(0), + ZERO_BYTES32 + ) + ); + vm.label(address(newPool), label); + + // Cannot set the pool creator directly on a standard Balancer stable pool factory. + vault.manualSetPoolCreator(address(newPool), lp); + + ProtocolFeeControllerMock feeController = ProtocolFeeControllerMock(address(vault.getProtocolFeeController())); + feeController.manualSetPoolCreator(address(newPool), lp); + + return address(newPool); + } + + function calculateMinAndMaxSwapAmounts() internal virtual override { + uint256 rateTokenA = getRate(tokenA); + uint256 rateTokenB = getRate(tokenB); + + // The vault does not allow trade amounts (amountGivenScaled18 or amountCalculatedScaled18) to be less than + // PRODUCTION_MIN_TRADE_AMOUNT. For "linear pools" (PoolMock), amountGivenScaled18 and amountCalculatedScaled18 + // are the same. So, minAmountGivenScaled18 > PRODUCTION_MIN_TRADE_AMOUNT. To derive the formula below, note + // that `amountGivenRaw = amountGivenScaled18/(rateToken * scalingFactor)`. There's an adjustment for stable + // math in the following steps. + uint256 tokenAMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenA).mulUp(10 ** decimalsTokenA); + uint256 tokenBMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenB).mulUp(10 ** decimalsTokenB); + + // Also, since we undo the operation (reverse swap with the output of the first swap), amountCalculatedRaw + // cannot be 0. Considering that amountCalculated is tokenB, and amountGiven is tokenA: + // 1) amountCalculatedRaw > 0 + // 2) amountCalculatedRaw = amountCalculatedScaled18 * 10^(decimalsB) / (rateB * 10^18) + // 3) amountCalculatedScaled18 = amountGivenScaled18 // Linear math, there's a factor to stable math + // 4) amountGivenScaled18 = amountGivenRaw * rateA * 10^18 / 10^(decimalsA) + // Combining the four formulas above, we determine that: + // amountCalculatedRaw > rateB * 10^(decimalsA) / (rateA * 10^(decimalsB)) + uint256 tokenACalculatedNotZero = (rateTokenB * (10 ** decimalsTokenA)) / (rateTokenA * (10 ** decimalsTokenB)); + uint256 tokenBCalculatedNotZero = (rateTokenA * (10 ** decimalsTokenB)) / (rateTokenB * (10 ** decimalsTokenA)); + + // Use the larger of the two values above to calculate the minSwapAmount. Also, multiply by 100 to account for + // swap fees and compensate for rate and math rounding issues. + uint256 mathFactor = 100; + minSwapAmountTokenA = ( + tokenAMinTradeAmount > tokenACalculatedNotZero + ? mathFactor * tokenAMinTradeAmount + : mathFactor * tokenACalculatedNotZero + ); + minSwapAmountTokenB = ( + tokenBMinTradeAmount > tokenBCalculatedNotZero + ? mathFactor * tokenBMinTradeAmount + : mathFactor * tokenBCalculatedNotZero + ); + + // 50% of pool init amount to make sure LP has enough tokens to pay for the swap in case of EXACT_OUT. + maxSwapAmountTokenA = poolInitAmountTokenA.mulDown(50e16); + maxSwapAmountTokenB = poolInitAmountTokenB.mulDown(50e16); + } +} From e3e7a5ae83310a7af38369f3a2ff0eed0cc0ef8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 15 Oct 2024 10:19:51 -0300 Subject: [PATCH 04/52] Fix invariant calculation on Gyro Pools --- pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol | 7 ++++--- pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol | 4 ++-- pkg/vault/test/foundry/E2eBatchSwap.t.sol | 20 +++++-------------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol index a532143d4..2a6edaf0f 100644 --- a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol @@ -47,6 +47,7 @@ library Gyro2CLPMath { sqrtBeta, rounding ); + return _calculateQuadratic(a, mb, bSquare, mc); } @@ -150,9 +151,9 @@ library Gyro2CLPMath { // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets // very slightly larger than 3e-18. uint256 virtInOver = balanceIn + virtualOffsetIn.mulUp(FixedPoint.ONE + 2); - uint256 virtOutUnder = balanceOut + virtualOffsetOut.mulDown(FixedPoint.ONE - 1); + uint256 virtOutUnder = balanceOut + (virtualOffsetOut).mulDown(FixedPoint.ONE - 1); - amountOut = virtOutUnder.mulDown(amountIn).divDown(virtInOver + amountIn); + amountOut = virtOutUnder.mulDown(amountIn).divDown(virtInOver + amountIn) - 1; } // This ensures amountOut < balanceOut. @@ -195,7 +196,7 @@ library Gyro2CLPMath { uint256 virtInOver = balanceIn + virtualOffsetIn.mulUp(FixedPoint.ONE + 2); uint256 virtOutUnder = balanceOut + virtualOffsetOut.mulDown(FixedPoint.ONE - 1); - amountIn = virtInOver.mulUp(amountOut).divUp(virtOutUnder - amountOut); + amountIn = virtInOver.mulUp(amountOut).divUp(virtOutUnder - amountOut) + 1; } } diff --git a/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol b/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol index 9356f2edf..2a7658ae6 100644 --- a/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol @@ -51,8 +51,8 @@ contract E2eBatchSwapGyro2CLPTest is E2eBatchSwapTest { Gyro2CLPPool newPool = Gyro2CLPPool( factory.create( - "Gyro 2CLP Pool", - "GRP", + label, + label, vault.buildTokenConfig(tokens.asIERC20()), _sqrtAlpha, _sqrtBeta, diff --git a/pkg/vault/test/foundry/E2eBatchSwap.t.sol b/pkg/vault/test/foundry/E2eBatchSwap.t.sol index 533f6f563..78590bf2d 100644 --- a/pkg/vault/test/foundry/E2eBatchSwap.t.sol +++ b/pkg/vault/test/foundry/E2eBatchSwap.t.sol @@ -141,27 +141,17 @@ contract E2eBatchSwapTest is BaseVaultTest { vm.startPrank(sender); uint256 amountOutDo = _executeAndCheckBatchExactIn(IERC20(address(tokenA)), exactAmountIn); - uint256 feesTokenD = vault.getAggregateSwapFeeAmount(poolC, tokenD); - uint256 amountOutUndo = _executeAndCheckBatchExactIn(IERC20(address(tokenD)), amountOutDo - feesTokenD); - uint256 feesTokenA = vault.getAggregateSwapFeeAmount(poolA, tokenA); - vm.stopPrank(); - assertGt(feesTokenA, 0, "No aggregate fees on tokenA (token in)"); - assertEq(feesTokenD, 0, "Aggregate fees on token D (token out)"); + uint256[] memory invariantsMid = _getPoolInvariants(); + uint256 amountOutUndo = _executeAndCheckBatchExactIn(IERC20(address(tokenD)), amountOutDo); + vm.stopPrank(); BaseVaultTest.Balances memory balancesAfter = getBalances(sender, tokensToTrack); uint256[] memory invariantsAfter = _getPoolInvariants(); - assertLe(amountOutUndo + feesTokenA, exactAmountIn, "Amount out undo should be <= exactAmountIn"); + assertLe(amountOutUndo, exactAmountIn, "Amount out undo should be <= exactAmountIn"); - _checkUserBalancesAndPoolInvariants( - balancesBefore, - balancesAfter, - invariantsBefore, - invariantsAfter, - 0, - feesTokenD - ); + _checkUserBalancesAndPoolInvariants(balancesBefore, balancesAfter, invariantsBefore, invariantsAfter, 0, 0); } function testDoUndoExactOut__Fuzz( From e05dcf285b375fe1df74792e50c066972bcd2196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 15 Oct 2024 10:22:13 -0300 Subject: [PATCH 05/52] Remove unused variable --- pkg/vault/test/foundry/E2eBatchSwap.t.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/vault/test/foundry/E2eBatchSwap.t.sol b/pkg/vault/test/foundry/E2eBatchSwap.t.sol index 78590bf2d..d395bc669 100644 --- a/pkg/vault/test/foundry/E2eBatchSwap.t.sol +++ b/pkg/vault/test/foundry/E2eBatchSwap.t.sol @@ -141,8 +141,6 @@ contract E2eBatchSwapTest is BaseVaultTest { vm.startPrank(sender); uint256 amountOutDo = _executeAndCheckBatchExactIn(IERC20(address(tokenA)), exactAmountIn); - - uint256[] memory invariantsMid = _getPoolInvariants(); uint256 amountOutUndo = _executeAndCheckBatchExactIn(IERC20(address(tokenD)), amountOutDo); vm.stopPrank(); From c99779df2023addf20fb79afd34391fae9879624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 15 Oct 2024 10:27:15 -0300 Subject: [PATCH 06/52] Fix README --- pkg/pool-gyro/README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/pool-gyro/README.md b/pkg/pool-gyro/README.md index 64d737619..27ed51220 100644 --- a/pkg/pool-gyro/README.md +++ b/pkg/pool-gyro/README.md @@ -2,11 +2,8 @@ # Balancer V3 Gyro Pools - -## Overview - - -### Usage +This package contains the source code for Gyro 2CLP pools ported to Balancer V3. For more information about this pool +type, please refer to the [documentation](https://docs.gyro.finance/gyroscope-protocol/concentrated-liquidity-pools/2-clps). ## Licensing From d5b491c46f3a01c5fb92e25570d32071c9b1497d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 15 Oct 2024 16:19:29 -0300 Subject: [PATCH 07/52] Fix E2EBatchSwap tests --- pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol | 4 ++-- pkg/vault/test/foundry/E2eBatchSwap.t.sol | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol index 2a6edaf0f..b36924338 100644 --- a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol @@ -153,7 +153,7 @@ library Gyro2CLPMath { uint256 virtInOver = balanceIn + virtualOffsetIn.mulUp(FixedPoint.ONE + 2); uint256 virtOutUnder = balanceOut + (virtualOffsetOut).mulDown(FixedPoint.ONE - 1); - amountOut = virtOutUnder.mulDown(amountIn).divDown(virtInOver + amountIn) - 1; + amountOut = virtOutUnder.mulDown(amountIn).divDown(virtInOver + amountIn); } // This ensures amountOut < balanceOut. @@ -196,7 +196,7 @@ library Gyro2CLPMath { uint256 virtInOver = balanceIn + virtualOffsetIn.mulUp(FixedPoint.ONE + 2); uint256 virtOutUnder = balanceOut + virtualOffsetOut.mulDown(FixedPoint.ONE - 1); - amountIn = virtInOver.mulUp(amountOut).divUp(virtOutUnder - amountOut) + 1; + amountIn = virtInOver.mulUp(amountOut).divUp(virtOutUnder - amountOut); } } diff --git a/pkg/vault/test/foundry/E2eBatchSwap.t.sol b/pkg/vault/test/foundry/E2eBatchSwap.t.sol index d395bc669..9db729099 100644 --- a/pkg/vault/test/foundry/E2eBatchSwap.t.sol +++ b/pkg/vault/test/foundry/E2eBatchSwap.t.sol @@ -137,7 +137,7 @@ contract E2eBatchSwapTest is BaseVaultTest { exactAmountIn = bound(exactAmountIn, minSwapAmountTokenA, maxSwapAmountTokenA); BaseVaultTest.Balances memory balancesBefore = getBalances(sender, tokensToTrack); - uint256[] memory invariantsBefore = _getPoolInvariants(); + uint256[] memory invariantsBefore = _getPoolInvariants(Rounding.ROUND_DOWN); vm.startPrank(sender); uint256 amountOutDo = _executeAndCheckBatchExactIn(IERC20(address(tokenA)), exactAmountIn); @@ -145,7 +145,7 @@ contract E2eBatchSwapTest is BaseVaultTest { vm.stopPrank(); BaseVaultTest.Balances memory balancesAfter = getBalances(sender, tokensToTrack); - uint256[] memory invariantsAfter = _getPoolInvariants(); + uint256[] memory invariantsAfter = _getPoolInvariants(Rounding.ROUND_UP); assertLe(amountOutUndo, exactAmountIn, "Amount out undo should be <= exactAmountIn"); @@ -163,7 +163,7 @@ contract E2eBatchSwapTest is BaseVaultTest { exactAmountOut = bound(exactAmountOut, minSwapAmountTokenD, maxSwapAmountTokenD); BaseVaultTest.Balances memory balancesBefore = getBalances(sender, tokensToTrack); - uint256[] memory invariantsBefore = _getPoolInvariants(); + uint256[] memory invariantsBefore = _getPoolInvariants(Rounding.ROUND_DOWN); vm.startPrank(sender); uint256 amountInDo = _executeAndCheckBatchExactOut(IERC20(address(tokenA)), exactAmountOut); @@ -176,7 +176,7 @@ contract E2eBatchSwapTest is BaseVaultTest { assertTrue(feesTokenD > 0, "No fees on tokenD"); BaseVaultTest.Balances memory balancesAfter = getBalances(sender, tokensToTrack); - uint256[] memory invariantsAfter = _getPoolInvariants(); + uint256[] memory invariantsAfter = _getPoolInvariants(Rounding.ROUND_UP); assertGe(amountInUndo, exactAmountOut + feesTokenD, "Amount in undo should be >= exactAmountOut"); @@ -452,13 +452,13 @@ contract E2eBatchSwapTest is BaseVaultTest { } } - function _getPoolInvariants() private view returns (uint256[] memory poolInvariants) { + function _getPoolInvariants(Rounding rounding) private view returns (uint256[] memory poolInvariants) { address[] memory pools = [poolA, poolB, poolC].toMemoryArray(); poolInvariants = new uint256[](pools.length); for (uint256 i = 0; i < pools.length; i++) { (, , , uint256[] memory lastBalancesLiveScaled18) = vault.getPoolTokenInfo(pools[i]); - poolInvariants[i] = IBasePool(pools[i]).computeInvariant(lastBalancesLiveScaled18, Rounding.ROUND_DOWN); + poolInvariants[i] = IBasePool(pools[i]).computeInvariant(lastBalancesLiveScaled18, rounding); } } From 93c9846ec17852839c39112fa6ab717917990f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 15 Oct 2024 17:25:15 -0300 Subject: [PATCH 08/52] Comment Gyro Math --- pkg/pool-gyro/contracts/Gyro2CLPPool.sol | 6 +++++- pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol | 22 +++++++++++++++----- pkg/pool-gyro/contracts/lib/GyroPoolMath.sol | 11 +++++++--- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/pkg/pool-gyro/contracts/Gyro2CLPPool.sol b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol index b275154b9..874d271e6 100644 --- a/pkg/pool-gyro/contracts/Gyro2CLPPool.sol +++ b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol @@ -15,7 +15,6 @@ import { import { BalancerPoolToken } from "@balancer-labs/v3-vault/contracts/BalancerPoolToken.sol"; import { PoolSwapParams, Rounding, SwapKind } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; -import "./lib/GyroPoolMath.sol"; import "./lib/Gyro2CLPMath.sol"; contract Gyro2CLPPool is IBasePool, BalancerPoolToken { @@ -80,6 +79,11 @@ contract Gyro2CLPPool is IBasePool, BalancerPoolToken { **********************************************************************************************/ uint256[2] memory sqrtParams = _sqrtParameters(); + + // `computeBalance` is used to calculate unbalanced adds and removes, when the BPT value is specified. + // A bigger invariant in `computeAddLiquiditySingleTokenExactOut` means that more tokens are required to + // fulfill the trade, and a bigger invariant in `computeRemoveLiquiditySingleTokenExactIn` means that the + // amount out is lower. So, the invariant should always be rounded up. uint256 invariant = Gyro2CLPMath._calculateInvariant( balancesLiveScaled18, sqrtParams[0], diff --git a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol index b36924338..c0d147db8 100644 --- a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol @@ -51,9 +51,15 @@ library Gyro2CLPMath { return _calculateQuadratic(a, mb, bSquare, mc); } - /** @dev Prepares quadratic terms for input to _calculateQuadratic - * works with a special case of quadratic that works nicely w/o negative numbers - * assumes a > 0, b < 0, and c <= 0 and returns a, -b, -c + /** + * @notice Prepares quadratic terms for input to _calculateQuadratic. + * @dev It works with a special case of quadratic that works nicely without negative numbers and assumes a > 0, + * b < 0, and c <= 0. + * + * @return a Bhaskara's `a` term + * @return mb Bhaskara's `b` term, negative (stands for minus b) + * @return bSquare Bhaskara's `b^2` term. The calculation is optimized to be more precise than just b*b + * @return mc Bhaskara's `c` term, negative (stands for minus c) */ function _calculateQuadraticTerms( uint256[] memory balances, @@ -69,14 +75,20 @@ library Gyro2CLPMath { : FixedPoint.mulUp; { + // `a` follows the opposite rounding than `b` and `c`, since the most significant term is in the + // denominator of Bhaskara's formula. To round invariant up, we need to round `a` down, which means that + // the division `sqrtAlpha/sqrtBeta` needs to be rounded up. a = FixedPoint.ONE - _divUpOrDown(sqrtAlpha, sqrtBeta); + + // `b` is a term in the numerator and should be rounded up if we want to increase the invariant. uint256 bterm0 = _divUpOrDown(balances[1], sqrtBeta); uint256 bterm1 = _mulUpOrDown(balances[0], sqrtAlpha); mb = bterm0 + bterm1; + // `c` is a term in the numerator and should be rounded up if we want to increase the invariant. mc = _mulUpOrDown(balances[0], balances[1]); } - // For better fixed point precision, calculate in expanded form w/ re-ordering of multiplications - // b^2 = x^2 * alpha + x*y*2*sqrt(alpha/beta) + y^2 / beta + // For better fixed point precision, calculate in expanded form re-ordering multiplications. + // `b^2 = x^2 * alpha + x*y*2*sqrt(alpha/beta) + y^2 / beta` bSquare = _mulUpOrDown(_mulUpOrDown(balances[0], balances[0]), _mulUpOrDown(sqrtAlpha, sqrtAlpha)); uint256 bSq2 = _divUpOrDown(2 * _mulUpOrDown(_mulUpOrDown(balances[0], balances[1]), sqrtAlpha), sqrtBeta); uint256 bSq3 = _divUpOrDown(_mulUpOrDown(balances[1], balances[1]), sqrtBeta.mulUp(sqrtBeta)); diff --git a/pkg/pool-gyro/contracts/lib/GyroPoolMath.sol b/pkg/pool-gyro/contracts/lib/GyroPoolMath.sol index 76cda8ec2..b05bdbfb2 100644 --- a/pkg/pool-gyro/contracts/lib/GyroPoolMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroPoolMath.sol @@ -27,7 +27,10 @@ library GyroPoolMath { uint256 guess = _makeInitialGuess(input); - // 7 iterations + // At this point `guess` is an estimation with one bit of precision. We know the true value is a uint128, + // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at + // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision + // into the expected uint128 result. guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; @@ -36,8 +39,10 @@ library GyroPoolMath { guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; guess = (guess + ((input * FixedPoint.ONE) / guess)) / 2; - // Check in some epsilon range - // Check square is more or less correct + // Check squaredGuess (guess * guess) is close enough from input. `guess` has less than 1 wei error, but the + // loss of precision in the 18 decimal representation causes an error in the squared number, which must be + // less than `(guess * tolerance) / FixedPoint.ONE`. Tolerance, in this case, is a very small number (< 10), so + // the tolerance will be very small too. uint256 guessSquared = guess.mulDown(guess); require( guessSquared <= input + guess.mulUp(tolerance) && guessSquared >= input - guess.mulUp(tolerance), From d86085d97f3b089e40f1f93c09278acb42531157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 15 Oct 2024 17:32:59 -0300 Subject: [PATCH 09/52] Fix mutability of test functions --- pkg/pool-gyro/test/foundry/ComputeBalance.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/pool-gyro/test/foundry/ComputeBalance.t.sol b/pkg/pool-gyro/test/foundry/ComputeBalance.t.sol index 2221ed946..ac7d6d0d6 100644 --- a/pkg/pool-gyro/test/foundry/ComputeBalance.t.sol +++ b/pkg/pool-gyro/test/foundry/ComputeBalance.t.sol @@ -29,7 +29,7 @@ contract ComputeBalanceTest is BaseVaultTest { vm.label(address(_gyroPool), "GyroPool"); } - function testComputeNewXBalance__Fuzz(uint256 balanceX, uint256 balanceY, uint256 deltaX) public { + function testComputeNewXBalance__Fuzz(uint256 balanceX, uint256 balanceY, uint256 deltaX) public view { balanceX = bound(balanceX, 1e16, 1e27); // Price range is [alpha,beta], so balanceY needs to be between alpha*balanceX and beta*balanceX balanceY = bound( @@ -54,7 +54,7 @@ contract ComputeBalanceTest is BaseVaultTest { assertApproxEqRel(newXBalance, balanceX + deltaX, 2e4); } - function testComputeNewYBalance__Fuzz(uint256 balanceX, uint256 balanceY, uint256 deltaY) public { + function testComputeNewYBalance__Fuzz(uint256 balanceX, uint256 balanceY, uint256 deltaY) public view { balanceX = bound(balanceX, 1e16, 1e27); // Price range is [alpha,beta], so balanceY needs to be between alpha*balanceX and beta*balanceX balanceY = bound( From 963e16f980e58016f7c12d976e22e7e5244576ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 15 Oct 2024 17:42:00 -0300 Subject: [PATCH 10/52] Fix comments of factory --- pkg/pool-gyro/contracts/Gyro2CLPPoolFactory.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/pool-gyro/contracts/Gyro2CLPPoolFactory.sol b/pkg/pool-gyro/contracts/Gyro2CLPPoolFactory.sol index 503d89903..220ca1ade 100644 --- a/pkg/pool-gyro/contracts/Gyro2CLPPoolFactory.sol +++ b/pkg/pool-gyro/contracts/Gyro2CLPPoolFactory.sol @@ -29,12 +29,15 @@ contract Gyro2CLPPoolFactory is BasePoolFactory { } /** - * @notice Deploys a new `StablePool`. + * @notice Deploys a new `Gyro2CLPPool`. * @param name The name of the pool * @param symbol The symbol of the pool * @param tokens An array of descriptors for the tokens the pool will manage * @param sqrtAlpha square root of first element in price range * @param sqrtBeta square root of last element in price range + * @param roleAccounts Addresses the Vault will allow to change certain pool settings + * @param swapFeePercentage Initial swap fee percentage + * @param poolHooksContract Contract that implements the hooks for the pool * @param salt The salt value that will be passed to create3 deployment */ function create( From 28e213dccf6bf395bb5c9fc808c0a476c80cf9bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 15 Oct 2024 17:51:42 -0300 Subject: [PATCH 11/52] First version of E-CLP pool --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 197 +++++ .../contracts/GyroECLPPoolFactory.sol | 84 ++ pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 717 ++++++++++++++++++ .../contracts/lib/SignedFixedPoint.sol | 253 ++++++ 4 files changed, 1251 insertions(+) create mode 100644 pkg/pool-gyro/contracts/GyroECLPPool.sol create mode 100644 pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol create mode 100644 pkg/pool-gyro/contracts/lib/GyroECLPMath.sol create mode 100644 pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol new file mode 100644 index 000000000..1a9e1c881 --- /dev/null +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: LicenseRef-Gyro-1.0 +// for information on licensing please see the README in the GitHub repository +// . + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; +import { BalancerPoolToken } from "@balancer-labs/v3-vault/contracts/BalancerPoolToken.sol"; +import { ISwapFeePercentageBounds } from "@balancer-labs/v3-interfaces/contracts/vault/ISwapFeePercentageBounds.sol"; +import { + IUnbalancedLiquidityInvariantRatioBounds +} from "@balancer-labs/v3-interfaces/contracts/vault/IUnbalancedLiquidityInvariantRatioBounds.sol"; +import { PoolSwapParams, Rounding, SwapKind } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import "./lib/GyroPoolMath.sol"; +import "./lib/GyroECLPMath.sol"; + +contract GyroECLPPool is IBasePool, BalancerPoolToken { + using FixedPoint for uint256; + + /// @dev Parameters of the ECLP pool + int256 internal immutable _paramsAlpha; + int256 internal immutable _paramsBeta; + int256 internal immutable _paramsC; + int256 internal immutable _paramsS; + int256 internal immutable _paramsLambda; + int256 internal immutable _tauAlphaX; + int256 internal immutable _tauAlphaY; + int256 internal immutable _tauBetaX; + int256 internal immutable _tauBetaY; + int256 internal immutable _u; + int256 internal immutable _v; + int256 internal immutable _w; + int256 internal immutable _z; + int256 internal immutable _dSq; + bytes32 private constant _POOL_TYPE = "ECLP"; + + struct GyroParams { + string name; + string symbol; + GyroECLPMath.Params eclpParams; + GyroECLPMath.DerivedParams derivedEclpParams; + } + + error SqrtParamsWrong(); + error SupportsOnlyTwoTokens(); + error NotImplemented(); + error AddressIsZeroAddress(); + + event ECLPParamsValidated(bool paramsValidated); + event ECLPDerivedParamsValidated(bool derivedParamsValidated); + event InvariantAterInitializeJoin(uint256 invariantAfterJoin); + event InvariantOldAndNew(uint256 oldInvariant, uint256 newInvariant); + + constructor(GyroParams memory params, IVault vault) BalancerPoolToken(vault, params.name, params.symbol) { + GyroECLPMath.validateParams(params.eclpParams); + emit ECLPParamsValidated(true); + + GyroECLPMath.validateDerivedParamsLimits(params.eclpParams, params.derivedEclpParams); + emit ECLPDerivedParamsValidated(true); + + (_paramsAlpha, _paramsBeta, _paramsC, _paramsS, _paramsLambda) = ( + params.eclpParams.alpha, + params.eclpParams.beta, + params.eclpParams.c, + params.eclpParams.s, + params.eclpParams.lambda + ); + + (_tauAlphaX, _tauAlphaY, _tauBetaX, _tauBetaY, _u, _v, _w, _z, _dSq) = ( + params.derivedEclpParams.tauAlpha.x, + params.derivedEclpParams.tauAlpha.y, + params.derivedEclpParams.tauBeta.x, + params.derivedEclpParams.tauBeta.y, + params.derivedEclpParams.u, + params.derivedEclpParams.v, + params.derivedEclpParams.w, + params.derivedEclpParams.z, + params.derivedEclpParams.dSq + ); + } + + /// @inheritdoc IBasePool + function computeInvariant(uint256[] memory balancesLiveScaled18, Rounding) public view returns (uint256) { + ( + GyroECLPMath.Params memory eclpParams, + GyroECLPMath.DerivedParams memory derivedECLPParams + ) = reconstructECLPParams(); + + return GyroECLPMath.calculateInvariant(balancesLiveScaled18, eclpParams, derivedECLPParams); + } + + /// @inheritdoc IBasePool + function computeBalance( + uint256[] memory balancesLiveScaled18, + uint256 tokenInIndex, + uint256 invariantRatio + ) external view returns (uint256 newBalance) { + computeInvariant(balancesLiveScaled18, Rounding.ROUND_UP); + + revert NotImplemented(); + } + + /// @inheritdoc IBasePool + function onSwap(PoolSwapParams memory request) public view onlyVault returns (uint256) { + bool tokenInIsToken0 = request.indexIn == 0; + + ( + GyroECLPMath.Params memory eclpParams, + GyroECLPMath.DerivedParams memory derivedECLPParams + ) = reconstructECLPParams(); + GyroECLPMath.Vector2 memory invariant; + { + (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( + request.balancesScaled18, + eclpParams, + derivedECLPParams + ); + // invariant = overestimate in x-component, underestimate in y-component + // No overflow in `+` due to constraints to the different values enforced in GyroECLPMath. + invariant = GyroECLPMath.Vector2(currentInvariant + 2 * invErr, currentInvariant); + } + + if (request.kind == SwapKind.EXACT_IN) { + uint256 amountOutScaled18 = GyroECLPMath.calcOutGivenIn( + request.balancesScaled18, + request.amountGivenScaled18, + tokenInIsToken0, + eclpParams, + derivedECLPParams, + invariant + ); + + return amountOutScaled18; + } else { + uint256 amountInScaled18 = GyroECLPMath.calcInGivenOut( + request.balancesScaled18, + request.amountGivenScaled18, + tokenInIsToken0, + eclpParams, + derivedECLPParams, + invariant + ); + + // Fees are added after scaling happens, to reduce the complexity of the rounding direction analysis. + return amountInScaled18; + } + } + + /** @dev reconstructs ECLP params structs from immutable arrays */ + function reconstructECLPParams() + internal + view + returns (GyroECLPMath.Params memory params, GyroECLPMath.DerivedParams memory d) + { + (params.alpha, params.beta, params.c, params.s, params.lambda) = ( + _paramsAlpha, + _paramsBeta, + _paramsC, + _paramsS, + _paramsLambda + ); + (d.tauAlpha.x, d.tauAlpha.y, d.tauBeta.x, d.tauBeta.y) = (_tauAlphaX, _tauAlphaY, _tauBetaX, _tauBetaY); + (d.u, d.v, d.w, d.z, d.dSq) = (_u, _v, _w, _z, _dSq); + } + + function getECLPParams() + external + view + returns (GyroECLPMath.Params memory params, GyroECLPMath.DerivedParams memory d) + { + return reconstructECLPParams(); + } + + /// @inheritdoc ISwapFeePercentageBounds + function getMinimumSwapFeePercentage() external pure returns (uint256) { + return 0; + } + + /// @inheritdoc ISwapFeePercentageBounds + function getMaximumSwapFeePercentage() external pure returns (uint256) { + return 1e18; + } + + /// @inheritdoc IUnbalancedLiquidityInvariantRatioBounds + function getMinimumInvariantRatio() external pure returns (uint256) { + return 0; + } + + /// @inheritdoc IUnbalancedLiquidityInvariantRatioBounds + function getMaximumInvariantRatio() external pure returns (uint256) { + return type(uint256).max; + } +} diff --git a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol new file mode 100644 index 000000000..ec5875596 --- /dev/null +++ b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { BasePoolFactory } from "@balancer-labs/v3-pool-utils/contracts/BasePoolFactory.sol"; + +import { GyroECLPPool } from "./GyroECLPPool.sol"; +import { GyroECLPMath } from "./lib/GyroECLPMath.sol"; + +/** + * @notice Gyro ECLP Pool factory + * @dev This is the most general factory, which allows two tokens. + */ +contract GyroECLPPoolFactory is BasePoolFactory { + // solhint-disable not-rely-on-time + + error SupportsOnlyTwoTokens(); + + constructor( + IVault vault, + uint32 pauseWindowDuration + ) BasePoolFactory(vault, pauseWindowDuration, type(GyroECLPPool).creationCode) { + // solhint-disable-previous-line no-empty-blocks + } + + /** + * @notice Deploys a new `GyroECLPPool`. + * @param name The name of the pool + * @param symbol The symbol of the pool + * @param tokens An array of descriptors for the tokens the pool will manage + * @param eclpParams parameters to configure the pool + * @param derivedEclpParams parameters with 38 decimals precision, to configure the pool + * @param roleAccounts Addresses the Vault will allow to change certain pool settings + * @param swapFeePercentage Initial swap fee percentage + * @param poolHooksContract Contract that implements the hooks for the pool + * @param salt The salt value that will be passed to create3 deployment + */ + function create( + string memory name, + string memory symbol, + TokenConfig[] memory tokens, + GyroECLPMath.Params memory eclpParams, + GyroECLPMath.DerivedParams memory derivedEclpParams, + PoolRoleAccounts memory roleAccounts, + uint256 swapFeePercentage, + address poolHooksContract, + bytes32 salt + ) external returns (address pool) { + if (tokens.length != 2) { + revert SupportsOnlyTwoTokens(); + } + + pool = _create( + abi.encode( + GyroECLPPool.GyroParams({ + name: name, + symbol: symbol, + eclpParams: eclpParams, + derivedEclpParams: derivedEclpParams + }), + getVault() + ), + salt + ); + + _registerPoolWithVault( + pool, + tokens, + swapFeePercentage, + false, // not exempt from protocol fees + roleAccounts, + poolHooksContract, + getDefaultLiquidityManagement() + ); + + _registerPoolWithFactory(pool); + } +} diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol new file mode 100644 index 000000000..f5e1199d1 --- /dev/null +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -0,0 +1,717 @@ +// SPDX-License-Identifier: LicenseRef-Gyro-1.0 +// for information on licensing please see the README in the GitHub repository . + +pragma solidity ^0.8.24; + + +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import "./SignedFixedPoint.sol"; +import "./GyroPoolMath.sol"; + +// solhint-disable private-vars-leading-underscore + +/** @dev ECLP math library. Pretty much a direct translation of the python version (see `tests/`). + * We use *signed* values here because some of the intermediate results can be negative (e.g. coordinates of points in + * the untransformed circle, "prices" in the untransformed circle). + */ +library GyroECLPMath { + error RotationVectorWrong(); + error PriceBoundsWrong(); + error RotationVectorNotNormalized(); + error AssetBoundsExceeded(); + error DerivedTauNotNormalized(); + error DerivedZetaWrong(); + error StretchingFactorWrong(); + error DerivedUvwzWrong(); + error InvariantDenominatorWrong(); + error MaxAssetsExceeded(); + error MaxInvariantExceeded(); + error DerivedDsqWrong(); + + uint256 internal constant ONEHALF = 0.5e18; + int256 internal constant ONE = 1e18; // 18 decimal places + int256 internal constant ONE_XP = 1e38; // 38 decimal places + + using SignedFixedPoint for int256; + using FixedPoint for uint256; + using SafeCast for uint256; + using SafeCast for int256; + + // Anti-overflow limits: Params and DerivedParams (static, only needs to be checked on pool creation) + int256 internal constant _ROTATION_VECTOR_NORM_ACCURACY = 1e3; // 1e-15 in normal precision + int256 internal constant _MAX_STRETCH_FACTOR = 1e26; // 1e8 in normal precision + int256 internal constant _DERIVED_TAU_NORM_ACCURACY_XP = 1e23; // 1e-15 in extra precision + int256 internal constant _MAX_INV_INVARIANT_DENOMINATOR_XP = 1e43; // 1e5 in extra precision + int256 internal constant _DERIVED_DSQ_NORM_ACCURACY_XP = 1e23; // 1e-15 in extra precision + + // Anti-overflow limits: Dynamic values (checked before operations that use them) + int256 internal constant _MAX_BALANCES = 1e34; // 1e16 in normal precision + int256 internal constant _MAX_INVARIANT = 3e37; // 3e19 in normal precision + + // Note that all t values (not tp or tpp) could consist of uint's, as could all Params. But it's complicated to + // convert all the time, so we make them all signed. We also store all intermediate values signed. An exception are + // the functions that are used by the contract b/c there the values are stored unsigned. + struct Params { + // Price bounds (lower and upper). 0 < alpha < beta + int256 alpha; + int256 beta; + // Rotation vector: + // phi in (-90 degrees, 0] is the implicit rotation vector. It's stored as a point: + int256 c; // c = cos(-phi) >= 0. rounded to 18 decimals + int256 s; // s = sin(-phi) >= 0. rounded to 18 decimals + // Invariant: c^2 + s^2 == 1, i.e., the point (c, s) is normalized. + // due to rounding, this may not = 1. The term dSq in DerivedParams corrects for this in extra precision + + // Stretching factor: + int256 lambda; // lambda >= 1 where lambda == 1 is the circle. + } + + // terms in this struct are stored in extra precision (38 decimals) with final decimal rounded down + struct DerivedParams { + Vector2 tauAlpha; + Vector2 tauBeta; + int256 u; // from (A chi)_y = lambda * u + v + int256 v; // from (A chi)_y = lambda * u + v + int256 w; // from (A chi)_x = w / lambda + z + int256 z; // from (A chi)_x = w / lambda + z + int256 dSq; // error in c^2 + s^2 = dSq, used to correct errors in c, s, tau, u,v,w,z calculations + //int256 dAlpha; // normalization constant for tau(alpha) + //int256 dBeta; // normalization constant for tau(beta) + } + + struct Vector2 { + int256 x; + int256 y; + } + + struct QParams { + int256 a; + int256 b; + int256 c; + } + + /** @dev Enforces limits and approximate normalization of the rotation vector. */ + function validateParams(Params memory params) internal pure { + if (0 > params.s || params.s > ONE) { + revert RotationVectorWrong(); + } + + if (0 > params.c || params.c > ONE) { + revert RotationVectorWrong(); + } + + Vector2 memory sc = Vector2(params.s, params.c); + int256 scnorm2 = scalarProd(sc, sc); // squared norm + + if (ONE - _ROTATION_VECTOR_NORM_ACCURACY > scnorm2 || scnorm2 > ONE + _ROTATION_VECTOR_NORM_ACCURACY) { + revert RotationVectorNotNormalized(); + } + + if (params.lambda < 0 || params.lambda > _MAX_STRETCH_FACTOR) { + revert StretchingFactorWrong(); + } + } + + /** @dev Enforces limits and approximate normalization of the derived values. + Does NOT check for internal consistency of 'derived' with 'params'. */ + function validateDerivedParamsLimits(Params memory params, DerivedParams memory derived) external pure { + int256 norm2; + norm2 = scalarProdXp(derived.tauAlpha, derived.tauAlpha); + + if (ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP > norm2 || norm2 > ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP) { + revert DerivedTauNotNormalized(); + } + + norm2 = scalarProdXp(derived.tauBeta, derived.tauBeta); + + if (ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP > norm2 || norm2 > ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP) { + revert DerivedTauNotNormalized(); + } + + if (derived.u > ONE_XP) revert DerivedUvwzWrong(); + if (derived.v > ONE_XP) revert DerivedUvwzWrong(); + if (derived.w > ONE_XP) revert DerivedUvwzWrong(); + if (derived.z > ONE_XP) revert DerivedUvwzWrong(); + + if (ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP > derived.dSq || derived.dSq > ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP) { + revert DerivedDsqWrong(); + } + + // NB No anti-overflow checks are required given the checks done above and in validateParams(). + int256 mulDenominator = ONE_XP.divXpU(calcAChiAChiInXp(params, derived) - ONE_XP); + + if (mulDenominator > _MAX_INV_INVARIANT_DENOMINATOR_XP) { + revert InvariantDenominatorWrong(); + } + } + + function scalarProd(Vector2 memory t1, Vector2 memory t2) internal pure returns (int256 ret) { + ret = t1.x.mulDownMag(t2.x) + t1.y.mulDownMag(t2.y); + } + + /// @dev scalar product for extra-precision values + function scalarProdXp(Vector2 memory t1, Vector2 memory t2) internal pure returns (int256 ret) { + ret = t1.x.mulXp(t2.x) + t1.y.mulXp(t2.y); + } + + // "Methods" for Params. We could put these into a separate library and import them via 'using' to get method call + // syntax. + + /** @dev Calculate A t where A is given in Section 2.2 + * This is reversing rotation and scaling of the ellipse (mapping back to circle) */ + function mulA(Params memory params, Vector2 memory tp) internal pure returns (Vector2 memory t) { + // NB: This function is only used inside calculatePrice(). This is why we can make two simplifications: + // 1. We don't correct for precision of s, c using d.dSq because that level of precision is not important in this context. + // 2. We don't need to check for over/underflow b/c these are impossible in that context and given the (checked) assumptions on the various values. + t.x = params.c.mulDownMagU(tp.x).divDownMagU(params.lambda) - params.s.mulDownMagU(tp.y).divDownMagU(params.lambda); + t.y = params.s.mulDownMagU(tp.x) + params.c.mulDownMagU(tp.y); + } + + /** @dev Calculate virtual offset a given invariant r. + * See calculation in Section 2.1.2 Computing reserve offsets + * Note that, in contrast to virtual reserve offsets in CPMM, these are *subtracted* from the real + * reserves, moving the curve to the upper-right. They can be positive or negative, but not both can be negative. + * Calculates a = r*(A^{-1}tau(beta))_x rounding up in signed direction + * Notice that error in r is scaled by lambda, and so rounding direction is important */ + function virtualOffset0( + Params memory p, + DerivedParams memory d, + Vector2 memory r // overestimate in x component, underestimate in y + ) internal pure returns (int256 a) { + // a = r lambda c tau(beta)_x + rs tau(beta)_y + // account for 1 factors of dSq (2 s,c factors) + int256 termXp = d.tauBeta.x.divXpU(d.dSq); + a = d.tauBeta.x > 0 + ? r.x.mulUpMagU(p.lambda).mulUpMagU(p.c).mulUpXpToNpU(termXp) + : r.y.mulDownMagU(p.lambda).mulDownMagU(p.c).mulUpXpToNpU(termXp); + + // use fact that tau(beta)_y > 0, so the required rounding direction is clear. + a = a + r.x.mulUpMagU(p.s).mulUpXpToNpU(d.tauBeta.y.divXpU(d.dSq)); + } + + /** @dev calculate virtual offset b given invariant r. + * Calculates b = r*(A^{-1}tau(alpha))_y rounding up in signed direction */ + function virtualOffset1( + Params memory p, + DerivedParams memory d, + Vector2 memory r // overestimate in x component, underestimate in y + ) internal pure returns (int256 b) { + // b = -r \lambda s tau(alpha)_x + rc tau(alpha)_y + // account for 1 factors of dSq (2 s,c factors) + int256 termXp = d.tauAlpha.x.divXpU(d.dSq); + b = (d.tauAlpha.x < 0) + ? r.x.mulUpMagU(p.lambda).mulUpMagU(p.s).mulUpXpToNpU(-termXp) + : (-r.y).mulDownMagU(p.lambda).mulDownMagU(p.s).mulUpXpToNpU(termXp); + + // use fact that tau(alpha)_y > 0, so the required rounding direction is clear. + b = b + r.x.mulUpMagU(p.c).mulUpXpToNpU(d.tauAlpha.y.divXpU(d.dSq)); + } + + /** Maximal value for the real reserves x when the respective other balance is 0 for given invariant + * See calculation in Section 2.1.2. Calculation is ordered here for precision, but error in r is magnified by lambda + * Rounds down in signed direction */ + function maxBalances0( + Params memory p, + DerivedParams memory d, + Vector2 memory r // overestimate in x-component, underestimate in y-component + ) internal pure returns (int256 xp) { + // x^+ = r lambda c (tau(beta)_x - tau(alpha)_x) + rs (tau(beta)_y - tau(alpha)_y) + // account for 1 factors of dSq (2 s,c factors) + int256 termXp1 = (d.tauBeta.x - d.tauAlpha.x).divXpU(d.dSq); // note tauBeta.x > tauAlpha.x, so this is > 0 and rounding direction is clear + int256 termXp2 = (d.tauBeta.y - d.tauAlpha.y).divXpU(d.dSq); // note this may be negative, but since tauBeta.y, tauAlpha.y >= 0, it is always in [-1, 1]. + xp = r.y.mulDownMagU(p.lambda).mulDownMagU(p.c).mulDownXpToNpU(termXp1); + xp = xp + (termXp2 > 0 ? r.y.mulDownMagU(p.s) : r.x.mulUpMagU(p.s)).mulDownXpToNpU(termXp2); + } + + /** Maximal value for the real reserves y when the respective other balance is 0 for given invariant + * See calculation in Section 2.1.2. Calculation is ordered here for precision, but erorr in r is magnified by lambda + * Rounds down in signed direction */ + function maxBalances1( + Params memory p, + DerivedParams memory d, + Vector2 memory r // overestimate in x-component, underestimate in y-component + ) internal pure returns (int256 yp) { + // y^+ = r lambda s (tau(beta)_x - tau(alpha)_x) + rc (tau(alpha)_y - tau(beta)_y) + // account for 1 factors of dSq (2 s,c factors) + int256 termXp1 = (d.tauBeta.x - d.tauAlpha.x).divXpU(d.dSq); // note tauBeta.x > tauAlpha.x + int256 termXp2 = (d.tauAlpha.y - d.tauBeta.y).divXpU(d.dSq); + yp = r.y.mulDownMagU(p.lambda).mulDownMagU(p.s).mulDownXpToNpU(termXp1); + yp = yp + (termXp2 > 0 ? r.y.mulDownMagU(p.c) : r.x.mulUpMagU(p.c)).mulDownXpToNpU(termXp2); + } + + /** @dev Compute the invariant 'r' corresponding to the given values. The invariant can't be negative, but + * we use a signed value to store it because all the other calculations are happening with signed ints, too. + * Computes r according to Prop 13 in 2.2.1 Initialization from Real Reserves + * orders operations to achieve best precision + * Returns an underestimate and a bound on error size. + * Enforces anti-overflow limits on balances and the computed invariant in the process. */ + function calculateInvariantWithError( + uint256[] memory balances, + Params memory params, + DerivedParams memory derived + ) public pure returns (int256, int256) { + (int256 x, int256 y) = (balances[0].toInt256(), balances[1].toInt256()); + + if (x + y > _MAX_BALANCES) { + revert MaxAssetsExceeded(); + } + + int256 AtAChi = calcAtAChi(x, y, params, derived); + (int256 sqrt, int256 err) = calcInvariantSqrt(x, y, params, derived); + // calculate the error in the square root term, separates cases based on sqrt >= 1/2 + // somedayTODO: can this be improved for cases of large balances (when xp error magnifies to np) + // Note: the minimum non-zero value of sqrt is 1e-9 since the minimum argument is 1e-18 + if (sqrt > 0) { + // err + 1 to account for O(eps_np) term ignored before + err = (err + 1).divUpMagU(2 * sqrt); + } else { + // in the false case here, the extra precision error does not magnify, and so the error inside the sqrt is O(1e-18) + // somedayTODO: The true case will almost surely never happen (can it be removed) + err = err > 0 ? GyroPoolMath.sqrt(err.toUint256(), 5).toInt256() : int256(1e9); + } + // calculate the error in the numerator, scale the error by 20 to be sure all possible terms accounted for + err = ((params.lambda.mulUpMagU(x + y) / ONE_XP) + err + 1) * 20; + + // A chi \cdot A chi > 1, so round it up to round denominator up + // denominator uses extra precision, so we do * 1/denominator so we are sure the calculation doesn't overflow + int256 mulDenominator = ONE_XP.divXpU(calcAChiAChiInXp(params, derived) - ONE_XP); + // NOTE: Anti-overflow limits on mulDenominator are checked on contract creation. + + // as alternative, could do, but could overflow: invariant = (AtAChi.add(sqrt) - err).divXp(denominator); + int256 invariant = (AtAChi + sqrt - err).mulDownXpToNpU(mulDenominator); + // error scales if denominator is small + // NB: This error calculation computes the error in the expression "numerator / denominator", but in this code + // we actually use the formula "numerator * (1 / denominator)" to compute the invariant. This affects this line + // and the one below. + err = err.mulUpXpToNpU(mulDenominator); + // account for relative error due to error in the denominator + // error in denominator is O(epsilon) if lambda<1e11, scale up by 10 to be sure we catch it, and add O(eps) + // error in denominator is lambda^2 * 2e-37 and scales relative to the result / denominator + // Scale by a constant to account for errors in the scaling factor itself and limited compounding. + // calculating lambda^2 w/o decimals so that the calculation will never overflow, the lost precision isn't important + err = err + ((invariant.mulUpXpToNpU(mulDenominator) * ((params.lambda * params.lambda) / 1e36)) * 40) / ONE_XP + 1; + + if (invariant + err > _MAX_INVARIANT) { + revert MaxInvariantExceeded(); + } + + return (invariant, err); + } + + function calculateInvariant( + uint256[] memory balances, + Params memory params, + DerivedParams memory derived + ) external pure returns (uint256 uinvariant) { + (int256 invariant, ) = calculateInvariantWithError(balances, params, derived); + uinvariant = invariant.toUint256(); + } + + /// @dev calculate At \cdot A chi, ignores rounding direction. We will later compensate for the rounding error. + function calcAtAChi( + int256 x, + int256 y, + Params memory p, + DerivedParams memory d + ) internal pure returns (int256 val) { + // to save gas, pre-compute dSq^2 as it will be used 3 times + int256 dSq2 = d.dSq.mulXpU(d.dSq); + + // (cx - sy) * (w/lambda + z) / lambda + // account for 2 factors of dSq (4 s,c factors) + int256 termXp = (d.w.divDownMagU(p.lambda) + d.z).divDownMagU(p.lambda).divXpU(dSq2); + val = (x.mulDownMagU(p.c) - y.mulDownMagU(p.s)).mulDownXpToNpU(termXp); + + // (x lambda s + y lambda c) * u, note u > 0 + int256 termNp = x.mulDownMagU(p.lambda).mulDownMagU(p.s) + y.mulDownMagU(p.lambda).mulDownMagU(p.c); + val = val + termNp.mulDownXpToNpU(d.u.divXpU(dSq2)); + + // (sx+cy) * v, note v > 0 + termNp = x.mulDownMagU(p.s) + y.mulDownMagU(p.c); + val = val + termNp.mulDownXpToNpU(d.v.divXpU(dSq2)); + } + + /// @dev calculates A chi \cdot A chi in extra precision + /// Note: this can be >1 (and involves factor of lambda^2). We can compute it in extra precision w/o overflowing b/c it will be + /// at most 38 + 16 digits (38 from decimals, 2*8 from lambda^2 if lambda=1e8) + /// Since we will only divide by this later, we will not need to worry about overflow in that operation if done in the right way + /// TODO: is rounding direction ok? + function calcAChiAChiInXp(Params memory p, DerivedParams memory d) internal pure returns (int256 val) { + // to save gas, pre-compute dSq^3 as it will be used 4 times + int256 dSq3 = d.dSq.mulXpU(d.dSq).mulXpU(d.dSq); + + // (A chi)_y^2 = lambda^2 u^2 + lambda 2 u v + v^2 + // account for 3 factors of dSq (6 s,c factors) + // SOMEDAY: In these calcs, a calculated value is multiplied by lambda and lambda^2, resp, which implies some + // error amplification. It's fine b/c we're doing it in extra precision here, but would still be nice if it + // could be avoided, perhaps by splitting up the numbers into a high and low part. + val = p.lambda.mulUpMagU((2 * d.u).mulXpU(d.v).divXpU(dSq3)); + // for lambda^2 u^2 factor in rounding error in u since lambda could be big + // Note: lambda^2 is multiplied at the end to be sure the calculation doesn't overflow, but this can lose some precision + val = val + ((d.u + 1).mulXpU(d.u + 1).divXpU(dSq3)).mulUpMagU(p.lambda).mulUpMagU(p.lambda); + // the next line converts from extre precision to normal precision post-computation while rounding up + val = val + (d.v).mulXpU(d.v).divXpU(dSq3); + + // (A chi)_x^2 = (w/lambda + z)^2 + // account for 3 factors of dSq (6 s,c factors) + int256 termXp = d.w.divUpMagU(p.lambda) + d.z; + val = val + termXp.mulXpU(termXp).divXpU(dSq3); + } + + /// @dev calculate -(At)_x ^2 (A chi)_y ^2 + (At)_x ^2, rounding down in signed direction + function calcMinAtxAChiySqPlusAtxSq( + int256 x, + int256 y, + Params memory p, + DerivedParams memory d + ) internal pure returns (int256 val) { + //////////////////////////////////////////////////////////////////////////////////// + // (At)_x^2 (A chi)_y^2 = (x^2 c^2 - xy2sc + y^2 s^2) (u^2 + 2uv/lambda + v^2/lambda^2) + // account for 4 factors of dSq (8 s,c factors) + // + // (At)_x^2 = (x^2 c^2 - xy2sc + y^2 s^2)/lambda^2 + // account for 1 factor of dSq (2 s,c factors) + //////////////////////////////////////////////////////////////////////////////////// + int256 termNp = x.mulUpMagU(x).mulUpMagU(p.c).mulUpMagU(p.c) + y.mulUpMagU(y).mulUpMagU(p.s).mulUpMagU(p.s); + termNp = termNp - x.mulDownMagU(y).mulDownMagU(p.c * 2).mulDownMagU(p.s); + + int256 termXp = d.u.mulXpU(d.u) + (2 * d.u).mulXpU(d.v).divDownMagU(p.lambda) + d.v.mulXpU(d.v).divDownMagU(p.lambda).divDownMagU(p.lambda); + termXp = termXp.divXpU(d.dSq.mulXpU(d.dSq).mulXpU(d.dSq).mulXpU(d.dSq)); + val = (-termNp).mulDownXpToNpU(termXp); + + // now calculate (At)_x^2 accounting for possible rounding error to round down + // need to do 1/dSq in a way so that there is no overflow for large balances + val = val + (termNp - 9).divDownMagU(p.lambda).divDownMagU(p.lambda).mulDownXpToNpU(SignedFixedPoint.ONE_XP.divXpU(d.dSq)); + } + + /// @dev calculate 2(At)_x * (At)_y * (A chi)_x * (A chi)_y, ignores rounding direction + // Note: this ignores rounding direction and is corrected for later + function calc2AtxAtyAChixAChiy( + int256 x, + int256 y, + Params memory p, + DerivedParams memory d + ) internal pure returns (int256 val) { + //////////////////////////////////////////////////////////////////////////////////// + // = ((x^2 - y^2)sc + yx(c^2-s^2)) * 2 * (zu + (wu + zv)/lambda + wv/lambda^2) + // account for 4 factors of dSq (8 s,c factors) + //////////////////////////////////////////////////////////////////////////////////// + int256 termNp = (x.mulDownMagU(x) - y.mulUpMagU(y)).mulDownMagU(2 * p.c).mulDownMagU(p.s); + int256 xy = y.mulDownMagU(2 * x); + termNp = termNp + xy.mulDownMagU(p.c).mulDownMagU(p.c) - xy.mulDownMagU(p.s).mulDownMagU(p.s); + + int256 termXp = d.z.mulXpU(d.u) + d.w.mulXpU(d.v).divDownMagU(p.lambda).divDownMagU(p.lambda); + termXp = termXp + (d.w.mulXpU(d.u) + d.z.mulXpU(d.v)).divDownMagU(p.lambda); + termXp = termXp.divXpU(d.dSq.mulXpU(d.dSq).mulXpU(d.dSq).mulXpU(d.dSq)); + + val = termNp.mulDownXpToNpU(termXp); + } + + /// @dev calculate -(At)_y ^2 (A chi)_x ^2 + (At)_y ^2, rounding down in signed direction + function calcMinAtyAChixSqPlusAtySq( + int256 x, + int256 y, + Params memory p, + DerivedParams memory d + ) internal pure returns (int256 val) { + //////////////////////////////////////////////////////////////////////////////////// + // (At)_y^2 (A chi)_x^2 = (x^2 s^2 + xy2sc + y^2 c^2) * (z^2 + 2zw/lambda + w^2/lambda^2) + // account for 4 factors of dSq (8 s,c factors) + // (At)_y^2 = (x^2 s^2 + xy2sc + y^2 c^2) + // account for 1 factor of dSq (2 s,c factors) + //////////////////////////////////////////////////////////////////////////////////// + int256 termNp = x.mulUpMagU(x).mulUpMagU(p.s).mulUpMagU(p.s) + y.mulUpMagU(y).mulUpMagU(p.c).mulUpMagU(p.c); + termNp = termNp + x.mulUpMagU(y).mulUpMagU(p.s * 2).mulUpMagU(p.c); + + int256 termXp = d.z.mulXpU(d.z) + d.w.mulXpU(d.w).divDownMagU(p.lambda).divDownMagU(p.lambda); + termXp = termXp + (2 * d.z).mulXpU(d.w).divDownMagU(p.lambda); + termXp = termXp.divXpU(d.dSq.mulXpU(d.dSq).mulXpU(d.dSq).mulXpU(d.dSq)); + val = (-termNp).mulDownXpToNpU(termXp); + + // now calculate (At)_y^2 accounting for possible rounding error to round down + // need to do 1/dSq in a way so that there is no overflow for large balances + val = val + (termNp - 9).mulDownXpToNpU(SignedFixedPoint.ONE_XP.divXpU(d.dSq)); + } + + /// @dev Rounds down. Also returns an estimate for the error of the term under the sqrt (!) and without the regular + /// normal-precision error of O(1e-18). + function calcInvariantSqrt( + int256 x, + int256 y, + Params memory p, + DerivedParams memory d + ) internal pure returns (int256 val, int256 err) { + val = calcMinAtxAChiySqPlusAtxSq(x, y, p, d) + calc2AtxAtyAChixAChiy(x, y, p, d); + val = val + calcMinAtyAChixSqPlusAtySq(x, y, p, d); + // error inside the square root is O((x^2 + y^2) * eps_xp) + O(eps_np), where eps_xp=1e-38, eps_np=1e-18 + // note that in terms of rounding down, error corrects for calc2AtxAtyAChixAChiy() + // however, we also use this error to correct the invariant for an overestimate in swaps, it is all the same order though + // Note the O(eps_np) term will be dealt with later, so not included yet + // Note that the extra precision term doesn't propagate unless balances are > 100b + err = (x.mulUpMagU(x) + y.mulUpMagU(y)) / 1e38; + // we will account for the error later after the square root + // mathematically, terms in square root > 0, so treat as 0 if it is < 0 b/c of rounding error + val = val > 0 ? GyroPoolMath.sqrt(val.toUint256(), 5).toInt256() : int256(0); + } + + /** @dev Spot price of token 0 in units of token 1. + * See Prop. 12 in 2.1.6 Computing Prices */ + function calcSpotPrice0in1( + uint256[] memory balances, + Params memory params, + DerivedParams memory derived, + int256 invariant + ) external pure returns (uint256 px) { + // shift by virtual offsets to get v(t) + Vector2 memory r = Vector2(invariant, invariant); // ignore r rounding for spot price, precision will be lost in TWAP anyway + Vector2 memory ab = Vector2(virtualOffset0(params, derived, r), virtualOffset1(params, derived, r)); + Vector2 memory vec = Vector2(balances[0].toInt256() - ab.x, balances[1].toInt256() - ab.y); + + // transform to circle to get Av(t) + vec = mulA(params, vec); + // compute prices on circle + Vector2 memory pc = Vector2(vec.x.divDownMagU(vec.y), ONE); + + // Convert prices back to ellipse + // NB: These operations check for overflow because the price pc[0] might be large when vex.y is small. + // SOMEDAY I think this probably can't actually happen due to our bounds on the different values. In this case we could do this unchecked as well. + int256 pgx = scalarProd(pc, mulA(params, Vector2(ONE, 0))); + px = pgx.divDownMag(scalarProd(pc, mulA(params, Vector2(0, ONE)))).toUint256(); + } + + /** @dev Check that post-swap balances obey maximal asset bounds + * newBalance = post-swap balance of one asset + * assetIndex gives the index of the provided asset (0 = X, 1 = Y) */ + function checkAssetBounds( + Params memory params, + DerivedParams memory derived, + Vector2 memory invariant, + int256 newBal, + uint8 assetIndex + ) internal pure { + if (assetIndex == 0) { + int256 xPlus = maxBalances0(params, derived, invariant); + if (!(newBal <= _MAX_BALANCES && newBal <= xPlus)) revert AssetBoundsExceeded(); + return; + } + { + int256 yPlus = maxBalances1(params, derived, invariant); + if (!(newBal <= _MAX_BALANCES && newBal <= yPlus)) revert AssetBoundsExceeded(); + } + } + + function calcOutGivenIn( + uint256[] memory balances, + uint256 amountIn, + bool tokenInIsToken0, + Params memory params, + DerivedParams memory derived, + Vector2 memory invariant + ) external pure returns (uint256 amountOut) { + function(int256, Params memory, DerivedParams memory, Vector2 memory) pure returns (int256) calcGiven; + uint8 ixIn; + uint8 ixOut; + if (tokenInIsToken0) { + ixIn = 0; + ixOut = 1; + calcGiven = calcYGivenX; + } else { + ixIn = 1; + ixOut = 0; + calcGiven = calcXGivenY; + } + + int256 balInNew = (balances[ixIn] + amountIn).toInt256(); // checked because amountIn is given by the user. + checkAssetBounds(params, derived, invariant, balInNew, ixIn); + int256 balOutNew = calcGiven(balInNew, params, derived, invariant); + // Make sub checked as an extra check against numerical error; but this really should never happen + amountOut = balances[ixOut] - balOutNew.toUint256(); + // The above line guarantees that amountOut <= balances[ixOut]. + } + + function calcInGivenOut( + uint256[] memory balances, + uint256 amountOut, + bool tokenInIsToken0, + Params memory params, + DerivedParams memory derived, + Vector2 memory invariant + ) external pure returns (uint256 amountIn) { + function(int256, Params memory, DerivedParams memory, Vector2 memory) pure returns (int256) calcGiven; + uint8 ixIn; + uint8 ixOut; + if (tokenInIsToken0) { + ixIn = 0; + ixOut = 1; + calcGiven = calcXGivenY; // this reverses compared to calcOutGivenIn + } else { + ixIn = 1; + ixOut = 0; + calcGiven = calcYGivenX; // this reverses compared to calcOutGivenIn + } + + if (!(amountOut <= balances[ixOut])) revert AssetBoundsExceeded(); + int256 balOutNew = (balances[ixOut] - amountOut).toInt256(); + int256 balInNew = calcGiven(balOutNew, params, derived, invariant); + // The checks in the following two lines should really always succeed; we keep them as extra safety against numerical error. + checkAssetBounds(params, derived, invariant, balInNew, ixIn); + amountIn = balInNew.toUint256() -balances[ixIn]; + } + + /** @dev Variables are named for calculating y given x + * to calculate x given y, change x->y, s->c, c->s, a_>b, b->a, tauBeta.x -> -tauAlpha.x, tauBeta.y -> tauAlpha.y + * calculates an overestimate of calculated reserve post-swap */ + function solveQuadraticSwap( + int256 lambda, + int256 x, + int256 s, + int256 c, + Vector2 memory r, // overestimate in x component, underestimate in y + Vector2 memory ab, + Vector2 memory tauBeta, + int256 dSq + ) internal pure returns (int256) { + // x component will round up, y will round down, use extra precision + Vector2 memory lamBar; + lamBar.x = SignedFixedPoint.ONE_XP - SignedFixedPoint.ONE_XP.divDownMagU(lambda).divDownMagU(lambda); + // Note: The following cannot become negative even with errors because we require lambda >= 1 and + // divUpMag returns the exact result if the quotient is representable in 18 decimals. + lamBar.y = SignedFixedPoint.ONE_XP - SignedFixedPoint.ONE_XP.divUpMagU(lambda).divUpMagU(lambda); + // using qparams struct to avoid "stack too deep" + QParams memory q; + // shift by the virtual offsets + // note that we want an overestimate of offset here so that -x'*lambar*s*c is overestimated in signed direction + // account for 1 factor of dSq (2 s,c factors) + int256 xp = x - ab.x; + if (xp > 0) { + q.b = (-xp).mulDownMagU(s).mulDownMagU(c).mulUpXpToNpU(lamBar.y.divXpU(dSq)); + } else { + q.b = (-xp).mulUpMagU(s).mulUpMagU(c).mulUpXpToNpU(lamBar.x.divXpU(dSq) + 1); + } + + // x component will round up, y will round down, use extra precision + // account for 1 factor of dSq (2 s,c factors) + Vector2 memory sTerm; + // we wil take sTerm = 1 - sTerm below, using multiple lines to avoid "stack too deep" + sTerm.x = lamBar.y.mulDownMagU(s).mulDownMagU(s).divXpU(dSq); + sTerm.y = lamBar.x.mulUpMagU(s); + sTerm.y = sTerm.y.mulUpMagU(s).divXpU(dSq + 1) + 1; // account for rounding error in dSq, divXp + sTerm = Vector2(SignedFixedPoint.ONE_XP - sTerm.x, SignedFixedPoint.ONE_XP - sTerm.y); + // ^^ NB: The components of sTerm are non-negative: We only need to worry about sTerm.y. This is non-negative b/c, because of bounds on lambda lamBar <= 1 - 1e-16, and division by dSq ensures we have enough precision so that rounding errors are never magnitude 1e-16. + + // now compute the argument of the square root + q.c = -calcXpXpDivLambdaLambda(x, r, lambda, s, c, tauBeta, dSq); + q.c = q.c + r.y.mulDownMagU(r.y).mulDownXpToNpU(sTerm.y); + // the square root is always being subtracted, so round it down to overestimate the end balance + // mathematically, terms in square root > 0, so treat as 0 if it is < 0 b/c of rounding error + q.c = q.c > 0 ? GyroPoolMath.sqrt(q.c.toUint256(), 5).toInt256() : int256(0); + + // calculate the result in q.a + if (q.b - q.c > 0) { + q.a = (q.b - q.c).mulUpXpToNpU(SignedFixedPoint.ONE_XP.divXpU(sTerm.y) + 1); + } else { + q.a = (q.b - q.c).mulUpXpToNpU(SignedFixedPoint.ONE_XP.divXpU(sTerm.x)); + } + + // lastly, add the offset, note that we want an overestimate of offset here + return q.a + ab.y; + } + + /** @dev Calculates x'x'/λ^2 where x' = x - b = x - r (A^{-1}tau(beta))_x + * calculates an overestimate + * to calculate y'y', change x->y, s->c, c->s, tauBeta.x -> -tauAlpha.x, tauBeta.y -> tauAlpha.y */ + function calcXpXpDivLambdaLambda( + int256 x, + Vector2 memory r, // overestimate in x component, underestimate in y + int256 lambda, + int256 s, + int256 c, + Vector2 memory tauBeta, + int256 dSq + ) internal pure returns (int256) { + ////////////////////////////////////////////////////////////////////////////////// + // x'x'/lambda^2 = r^2 c^2 tau(beta)_x^2 + // + ( r^2 2s c tau(beta)_x tau(beta)_y - rx 2c tau(beta)_x ) / lambda + // + ( r^2 s^2 tau(beta)_y^2 - rx 2s tau(beta)_y + x^2 ) / lambda^2 + ////////////////////////////////////////////////////////////////////////////////// + // to save gas, pre-compute dSq^2 as it will be used 3 times, and r.x^2 as it will be used 2-3 times + // sqVars = (dSq^2, r.x^2) + Vector2 memory sqVars = Vector2(dSq.mulXpU(dSq), r.x.mulUpMagU(r.x)); + + QParams memory q; // for working terms + // q.a = r^2 s 2c tau(beta)_x tau(beta)_y + // account for 2 factors of dSq (4 s,c factors) + int256 termXp = tauBeta.x.mulXpU(tauBeta.y).divXpU(sqVars.x); + if (termXp > 0) { + q.a = sqVars.y.mulUpMagU(2 * s); + q.a = q.a.mulUpMagU(c).mulUpXpToNpU(termXp + 7); // +7 account for rounding in termXp + } else { + q.a = r.y.mulDownMagU(r.y).mulDownMagU(2 * s); + q.a = q.a.mulDownMagU(c).mulUpXpToNpU(termXp); + } + + // -rx 2c tau(beta)_x + // account for 1 factor of dSq (2 s,c factors) + if (tauBeta.x < 0) { + // +3 account for rounding in extra precision terms + q.b = r.x.mulUpMagU(x).mulUpMagU(2 * c).mulUpXpToNpU(-tauBeta.x.divXpU(dSq) + 3); + } else { + q.b = (-r.y).mulDownMagU(x).mulDownMagU(2 * c).mulUpXpToNpU(tauBeta.x.divXpU(dSq)); + } + // q.a later needs to be divided by lambda + q.a = q.a + q.b; + + // q.b = r^2 s^2 tau(beta)_y^2 + // account for 2 factors of dSq (4 s,c factors) + termXp = tauBeta.y.mulXpU(tauBeta.y).divXpU(sqVars.x) + 7; // +7 account for rounding in termXp + q.b = sqVars.y.mulUpMagU(s); + q.b = q.b.mulUpMagU(s).mulUpXpToNpU(termXp); + + // q.c = -rx 2s tau(beta)_y, recall that tauBeta.y > 0 so round lower in magnitude + // account for 1 factor of dSq (2 s,c factors) + q.c = (-r.y).mulDownMagU(x).mulDownMagU(2 * s).mulUpXpToNpU(tauBeta.y.divXpU(dSq)); + + // (q.b + q.c + x^2) / lambda + q.b = q.b + q.c + x.mulUpMagU(x); + q.b = q.b > 0 ? q.b.divUpMagU(lambda) : q.b.divDownMagU(lambda); + + // remaining calculation is (q.a + q.b) / lambda + q.a = q.a + q.b; + q.a = q.a > 0 ? q.a.divUpMagU(lambda) : q.a.divDownMagU(lambda); + + // + r^2 c^2 tau(beta)_x^2 + // account for 2 factors of dSq (4 s,c factors) + termXp = tauBeta.x.mulXpU(tauBeta.x).divXpU(sqVars.x) + 7; // +7 account for rounding in termXp + int256 val = sqVars.y.mulUpMagU(c).mulUpMagU(c); + return (val.mulUpXpToNpU(termXp)) + q.a; + } + + /** @dev compute y such that (x, y) satisfy the invariant at the given parameters. + * Note that we calculate an overestimate of y + * See Prop 14 in section 2.2.2 Trade Execution */ + function calcYGivenX( + int256 x, + Params memory params, + DerivedParams memory d, + Vector2 memory r // overestimate in x component, underestimate in y + ) internal pure returns (int256 y) { + // want to overestimate the virtual offsets except in a particular setting that will be corrected for later + // note that the error correction in the invariant should more than make up for uncaught rounding directions (in 38 decimals) in virtual offsets + Vector2 memory ab = Vector2(virtualOffset0(params, d, r), virtualOffset1(params, d, r)); + y = solveQuadraticSwap(params.lambda, x, params.s, params.c, r, ab, d.tauBeta, d.dSq); + } + + function calcXGivenY( + int256 y, + Params memory params, + DerivedParams memory d, + Vector2 memory r // overestimate in x component, underestimate in y + ) internal pure returns (int256 x) { + // want to overestimate the virtual offsets except in a particular setting that will be corrected for later + // note that the error correction in the invariant should more than make up for uncaught rounding directions (in 38 decimals) in virtual offsets + Vector2 memory ba = Vector2(virtualOffset1(params, d, r), virtualOffset0(params, d, r)); + // change x->y, s->c, c->s, b->a, a->b, tauBeta.x -> -tauAlpha.x, tauBeta.y -> tauAlpha.y vs calcYGivenX + x = solveQuadraticSwap(params.lambda, y, params.c, params.s, r, ba, Vector2(-d.tauAlpha.x, d.tauAlpha.y), d.dSq); + } +} diff --git a/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol b/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol new file mode 100644 index 000000000..0a5fd72bf --- /dev/null +++ b/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: LicenseRef-Gyro-1.0 +// for information on licensing please see the README in the GitHub repository . + +pragma solidity ^0.8.24; + +/* solhint-disable private-vars-leading-underscore */ + +/// @dev Signed fixed point operations based on Balancer's FixedPoint library. +/// Note: The `{mul,div}{UpMag,DownMag}()` functions do *not* round up or down, respectively, +/// in a signed fashion (like ceil and floor operations), but *in absolute value* (or *magnitude*), i.e., +/// towards 0. This is useful in some applications. +library SignedFixedPoint { + error AddOverflow(); + error SubOverflow(); + error MulOverflow(); + error ZeroDivision(); + error DivInterval(); + + int256 internal constant ONE = 1e18; // 18 decimal places + // setting extra precision at 38 decimals, which is the most we can get w/o overflowing on normal multiplication + // this allows 20 extra digits to absorb error when multiplying by large numbers + int256 internal constant ONE_XP = 1e38; // 38 decimal places + + function add(int256 a, int256 b) internal pure returns (int256) { + // Fixed Point addition is the same as regular checked addition + + int256 c = a + b; + if (!(b >= 0 ? c >= a : c < a)) revert AddOverflow(); + return c; + } + + function addMag(int256 a, int256 b) internal pure returns (int256 c) { + // add b in the same signed direction as a, i.e. increase the magnitude of a by b + c = a > 0 ? add(a, b) : sub(a, b); + } + + function sub(int256 a, int256 b) internal pure returns (int256) { + // Fixed Point subtraction is the same as regular checked subtraction + + int256 c = a - b; + if (!(b <= 0 ? c >= a : c < a)) revert SubOverflow(); + return c; + } + + /// @dev This rounds towards 0, i.e., down *in absolute value*! + function mulDownMag(int256 a, int256 b) internal pure returns (int256) { + int256 product = a * b; + if (!(a == 0 || product / a == b)) revert MulOverflow(); + + return product / ONE; + } + + /// @dev this implements mulDownMag w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + function mulDownMagU(int256 a, int256 b) internal pure returns (int256) { + return (a * b) / ONE; + } + + /// @dev This rounds away from 0, i.e., up *in absolute value*! + function mulUpMag(int256 a, int256 b) internal pure returns (int256) { + int256 product = a * b; + if (!(a == 0 || product / a == b)) revert MulOverflow(); + + // If product > 0, the result should be ceil(p/ONE) = floor((p-1)/ONE) + 1, where floor() is implicit. If + // product < 0, the result should be floor(p/ONE) = ceil((p+1)/ONE) - 1, where ceil() is implicit. + // Addition for signed numbers: Case selection so we round away from 0, not always up. + if (product > 0) return ((product - 1) / ONE) + 1; + else if (product < 0) return ((product + 1) / ONE) - 1; + // product == 0 + return 0; + } + + /// @dev this implements mulUpMag w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + function mulUpMagU(int256 a, int256 b) internal pure returns (int256) { + int256 product = a * b; + + // If product > 0, the result should be ceil(p/ONE) = floor((p-1)/ONE) + 1, where floor() is implicit. If + // product < 0, the result should be floor(p/ONE) = ceil((p+1)/ONE) - 1, where ceil() is implicit. + // Addition for signed numbers: Case selection so we round away from 0, not always up. + if (product > 0) return ((product - 1) / ONE) + 1; + else if (product < 0) return ((product + 1) / ONE) - 1; + // product == 0 + return 0; + } + + /// @dev Rounds towards 0, i.e., down in absolute value. + function divDownMag(int256 a, int256 b) internal pure returns (int256) { + if (b == 0) revert ZeroDivision(); + + if (a == 0) { + return 0; + } + + int256 aInflated = a * ONE; + if (aInflated / a != ONE) revert DivInterval(); + + return aInflated / b; + } + + /// @dev this implements divDownMag w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + function divDownMagU(int256 a, int256 b) internal pure returns (int256) { + if (b == 0) revert ZeroDivision(); + return (a * ONE) / b; + } + + /// @dev Rounds away from 0, i.e., up in absolute value. + function divUpMag(int256 a, int256 b) internal pure returns (int256) { + if (b == 0) revert ZeroDivision(); + + if (a == 0) { + return 0; + } + + if (b < 0) { + // Required so the below is correct. + b = -b; + a = -a; + } + + int256 aInflated = a * ONE; + if (aInflated / a != ONE) revert DivInterval(); + + if (aInflated > 0) return ((aInflated - 1) / b) + 1; + return ((aInflated + 1) / b) - 1; + } + + /// @dev this implements divUpMag w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + function divUpMagU(int256 a, int256 b) internal pure returns (int256) { + if (b == 0) revert ZeroDivision(); + + if (a == 0) { + return 0; + } + + // SOMEDAY check if we can shave off some gas by logically refactoring this vs the below case distinction into one (on a * b or so). + if (b < 0) { + // Ensure b > 0 so the below is correct. + b = -b; + a = -a; + } + + if (a > 0) return ((a * ONE - 1) / b) + 1; + return ((a * ONE + 1) / b) - 1; + } + + /// @dev multiplies two extra precision numbers (with 38 decimals) + /// rounds down in magnitude but this shouldn't matter + /// multiplication can overflow if a,b are > 2 in magnitude + function mulXp(int256 a, int256 b) internal pure returns (int256) { + int256 product = a * b; + if (!(a == 0 || product / a == b)) revert MulOverflow(); + + return product / ONE_XP; + } + + /// @dev multiplies two extra precision numbers (with 38 decimals) + /// rounds down in magnitude but this shouldn't matter + /// multiplication can overflow if a,b are > 2 in magnitude + /// this implements mulXp w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + function mulXpU(int256 a, int256 b) internal pure returns (int256) { + return (a * b) / ONE_XP; + } + + /// @dev divides two extra precision numbers (with 38 decimals) + /// rounds down in magnitude but this shouldn't matter + /// can overflow if a > 2 or b << 1 in magnitude + function divXp(int256 a, int256 b) internal pure returns (int256) { + if (b == 0) revert ZeroDivision(); + + if (a == 0) { + return 0; + } + + int256 aInflated = a * ONE_XP; + if (aInflated / a != ONE_XP) revert DivInterval(); + + return aInflated / b; + } + + /// @dev divides two extra precision numbers (with 38 decimals) + /// rounds down in magnitude but this shouldn't matter + /// can overflow if a > 2 or b << 1 in magnitude + /// this implements divXp w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + function divXpU(int256 a, int256 b) internal pure returns (int256) { + if (b == 0) revert ZeroDivision(); + + return (a * ONE_XP) / b; + } + + /// @dev multiplies normal precision a with extra precision b (with 38 decimals) + /// Rounds down in signed direction + /// returns normal precision of the product + function mulDownXpToNp(int256 a, int256 b) internal pure returns (int256) { + int256 b1 = b / 1e19; + int256 prod1 = a * b1; + if (!(a == 0 || prod1 / a == b1)) revert MulOverflow(); + int256 b2 = b % 1e19; + int256 prod2 = a * b2; + if (!(a == 0 || prod2 / a == b2)) revert MulOverflow(); + return prod1 >= 0 && prod2 >= 0 ? (prod1 + prod2 / 1e19) / 1e19 : (prod1 + prod2 / 1e19 + 1) / 1e19 - 1; + } + + /// @dev multiplies normal precision a with extra precision b (with 38 decimals) + /// Rounds down in signed direction + /// returns normal precision of the product + /// this implements mulDownXpToNp w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + function mulDownXpToNpU(int256 a, int256 b) internal pure returns (int256) { + int256 b1 = b / 1e19; + int256 b2 = b % 1e19; + // SOMEDAY check if we eliminate these vars and save some gas (by only checking the sign of prod1, say) + int256 prod1 = a * b1; + int256 prod2 = a * b2; + return prod1 >= 0 && prod2 >= 0 ? (prod1 + prod2 / 1e19) / 1e19 : (prod1 + prod2 / 1e19 + 1) / 1e19 - 1; + } + + /// @dev multiplies normal precision a with extra precision b (with 38 decimals) + /// Rounds up in signed direction + /// returns normal precision of the product + function mulUpXpToNp(int256 a, int256 b) internal pure returns (int256) { + int256 b1 = b / 1e19; + int256 prod1 = a * b1; + if (!(a == 0 || prod1 / a == b1)) revert MulOverflow(); + int256 b2 = b % 1e19; + int256 prod2 = a * b2; + if (!(a == 0 || prod2 / a == b2)) revert MulOverflow(); + return prod1 <= 0 && prod2 <= 0 ? (prod1 + prod2 / 1e19) / 1e19 : (prod1 + prod2 / 1e19 - 1) / 1e19 + 1; + } + + /// @dev multiplies normal precision a with extra precision b (with 38 decimals) + /// Rounds up in signed direction + /// returns normal precision of the product + /// this implements mulUpXpToNp w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + function mulUpXpToNpU(int256 a, int256 b) internal pure returns (int256) { + int256 b1 = b / 1e19; + int256 b2 = b % 1e19; + // SOMEDAY check if we eliminate these vars and save some gas (by only checking the sign of prod1, say) + int256 prod1 = a * b1; + int256 prod2 = a * b2; + return prod1 <= 0 && prod2 <= 0 ? (prod1 + prod2 / 1e19) / 1e19 : (prod1 + prod2 / 1e19 - 1) / 1e19 + 1; + } + + // not implementing the pow functions right now b/c it's annoying and slightly ill-defined, and we don't use them. + + /** + * @dev Returns the complement of a value (1 - x), capped to 0 if x is larger than 1. + * + * Useful when computing the complement for values with some level of relative error, as it strips this error and + * prevents intermediate negative values. + */ + function complement(int256 x) internal pure returns (int256) { + if (x >= ONE || x <= 0) return 0; + return ONE - x; + } +} From 51202d8368dea133ecb8ccc17e1f25ad3c04a25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 15 Oct 2024 20:06:01 -0300 Subject: [PATCH 12/52] Implement computeBalance --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 35 ++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 1a9e1c881..b0c11287f 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.24; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; @@ -20,6 +22,7 @@ import "./lib/GyroECLPMath.sol"; contract GyroECLPPool is IBasePool, BalancerPoolToken { using FixedPoint for uint256; + using SafeCast for *; /// @dev Parameters of the ECLP pool int256 internal immutable _paramsAlpha; @@ -99,9 +102,37 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { uint256 tokenInIndex, uint256 invariantRatio ) external view returns (uint256 newBalance) { - computeInvariant(balancesLiveScaled18, Rounding.ROUND_UP); + ( + GyroECLPMath.Params memory eclpParams, + GyroECLPMath.DerivedParams memory derivedECLPParams + ) = reconstructECLPParams(); - revert NotImplemented(); + GyroECLPMath.Vector2 memory invariant; + { + (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( + balancesLiveScaled18, + eclpParams, + derivedECLPParams + ); + + // invariant = overestimate in x-component, underestimate in y-component. + invariant = GyroECLPMath.Vector2( + (currentInvariant + 2 * invErr).toUint256().mulUp(invariantRatio).toInt256(), + currentInvariant.toUint256().mulUp(invariantRatio).toInt256() + ); + } + + if (tokenInIndex == 0) { + return + GyroECLPMath + .calcXGivenY(balancesLiveScaled18[1].toInt256(), eclpParams, derivedECLPParams, invariant) + .toUint256(); + } else { + return + GyroECLPMath + .calcYGivenX(balancesLiveScaled18[0].toInt256(), eclpParams, derivedECLPParams, invariant) + .toUint256(); + } } /// @inheritdoc IBasePool From 61829165f9a38c1b61490596a82b215fad259569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 15 Oct 2024 20:25:04 -0300 Subject: [PATCH 13/52] Implement LiquidityApprox test for E-CLP --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 14 ++- pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 49 +++++++---- ...BatchSwap.t.sol => E2eBatchSwap2CLP.t.sol} | 0 .../{E2eSwap.t.sol => E2eSwap2CLP.t.sol} | 0 ...er.t.sol => E2eSwapRateProvider2CLP.t.sol} | 0 ...t.sol => LiquidityApproximation2CLP.t.sol} | 0 .../foundry/LiquidityApproximationECLP.t.sol | 86 +++++++++++++++++++ 7 files changed, 129 insertions(+), 20 deletions(-) rename pkg/pool-gyro/test/foundry/{E2eBatchSwap.t.sol => E2eBatchSwap2CLP.t.sol} (100%) rename pkg/pool-gyro/test/foundry/{E2eSwap.t.sol => E2eSwap2CLP.t.sol} (100%) rename pkg/pool-gyro/test/foundry/{E2eSwapRateProvider.t.sol => E2eSwapRateProvider2CLP.t.sol} (100%) rename pkg/pool-gyro/test/foundry/{LiquidityApproximationGyro.t.sol => LiquidityApproximation2CLP.t.sol} (100%) create mode 100644 pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index b0c11287f..feb9514ce 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -87,13 +87,23 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { } /// @inheritdoc IBasePool - function computeInvariant(uint256[] memory balancesLiveScaled18, Rounding) public view returns (uint256) { + function computeInvariant(uint256[] memory balancesLiveScaled18, Rounding rounding) public view returns (uint256) { ( GyroECLPMath.Params memory eclpParams, GyroECLPMath.DerivedParams memory derivedECLPParams ) = reconstructECLPParams(); - return GyroECLPMath.calculateInvariant(balancesLiveScaled18, eclpParams, derivedECLPParams); + (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( + balancesLiveScaled18, + eclpParams, + derivedECLPParams + ); + + if (rounding == Rounding.ROUND_DOWN) { + return currentInvariant.toUint256(); + } else { + return (currentInvariant + 2 * invErr).toUint256(); + } } /// @inheritdoc IBasePool diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index f5e1199d1..f3285a8f3 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.24; - import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; import "./SignedFixedPoint.sol"; @@ -134,14 +133,16 @@ library GyroECLPMath { if (derived.w > ONE_XP) revert DerivedUvwzWrong(); if (derived.z > ONE_XP) revert DerivedUvwzWrong(); - if (ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP > derived.dSq || derived.dSq > ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP) { + if ( + ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP > derived.dSq || derived.dSq > ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP + ) { revert DerivedDsqWrong(); } // NB No anti-overflow checks are required given the checks done above and in validateParams(). int256 mulDenominator = ONE_XP.divXpU(calcAChiAChiInXp(params, derived) - ONE_XP); - if (mulDenominator > _MAX_INV_INVARIANT_DENOMINATOR_XP) { + if (mulDenominator > _MAX_INV_INVARIANT_DENOMINATOR_XP) { revert InvariantDenominatorWrong(); } } @@ -164,7 +165,9 @@ library GyroECLPMath { // NB: This function is only used inside calculatePrice(). This is why we can make two simplifications: // 1. We don't correct for precision of s, c using d.dSq because that level of precision is not important in this context. // 2. We don't need to check for over/underflow b/c these are impossible in that context and given the (checked) assumptions on the various values. - t.x = params.c.mulDownMagU(tp.x).divDownMagU(params.lambda) - params.s.mulDownMagU(tp.y).divDownMagU(params.lambda); + t.x = + params.c.mulDownMagU(tp.x).divDownMagU(params.lambda) - + params.s.mulDownMagU(tp.y).divDownMagU(params.lambda); t.y = params.s.mulDownMagU(tp.x) + params.c.mulDownMagU(tp.y); } @@ -290,7 +293,11 @@ library GyroECLPMath { // error in denominator is lambda^2 * 2e-37 and scales relative to the result / denominator // Scale by a constant to account for errors in the scaling factor itself and limited compounding. // calculating lambda^2 w/o decimals so that the calculation will never overflow, the lost precision isn't important - err = err + ((invariant.mulUpXpToNpU(mulDenominator) * ((params.lambda * params.lambda) / 1e36)) * 40) / ONE_XP + 1; + err = + err + + ((invariant.mulUpXpToNpU(mulDenominator) * ((params.lambda * params.lambda) / 1e36)) * 40) / + ONE_XP + + 1; if (invariant + err > _MAX_INVARIANT) { revert MaxInvariantExceeded(); @@ -299,15 +306,6 @@ library GyroECLPMath { return (invariant, err); } - function calculateInvariant( - uint256[] memory balances, - Params memory params, - DerivedParams memory derived - ) external pure returns (uint256 uinvariant) { - (int256 invariant, ) = calculateInvariantWithError(balances, params, derived); - uinvariant = invariant.toUint256(); - } - /// @dev calculate At \cdot A chi, ignores rounding direction. We will later compensate for the rounding error. function calcAtAChi( int256 x, @@ -376,13 +374,19 @@ library GyroECLPMath { int256 termNp = x.mulUpMagU(x).mulUpMagU(p.c).mulUpMagU(p.c) + y.mulUpMagU(y).mulUpMagU(p.s).mulUpMagU(p.s); termNp = termNp - x.mulDownMagU(y).mulDownMagU(p.c * 2).mulDownMagU(p.s); - int256 termXp = d.u.mulXpU(d.u) + (2 * d.u).mulXpU(d.v).divDownMagU(p.lambda) + d.v.mulXpU(d.v).divDownMagU(p.lambda).divDownMagU(p.lambda); + int256 termXp = d.u.mulXpU(d.u) + + (2 * d.u).mulXpU(d.v).divDownMagU(p.lambda) + + d.v.mulXpU(d.v).divDownMagU(p.lambda).divDownMagU(p.lambda); termXp = termXp.divXpU(d.dSq.mulXpU(d.dSq).mulXpU(d.dSq).mulXpU(d.dSq)); val = (-termNp).mulDownXpToNpU(termXp); // now calculate (At)_x^2 accounting for possible rounding error to round down // need to do 1/dSq in a way so that there is no overflow for large balances - val = val + (termNp - 9).divDownMagU(p.lambda).divDownMagU(p.lambda).mulDownXpToNpU(SignedFixedPoint.ONE_XP.divXpU(d.dSq)); + val = + val + + (termNp - 9).divDownMagU(p.lambda).divDownMagU(p.lambda).mulDownXpToNpU( + SignedFixedPoint.ONE_XP.divXpU(d.dSq) + ); } /// @dev calculate 2(At)_x * (At)_y * (A chi)_x * (A chi)_y, ignores rounding direction @@ -556,7 +560,7 @@ library GyroECLPMath { int256 balInNew = calcGiven(balOutNew, params, derived, invariant); // The checks in the following two lines should really always succeed; we keep them as extra safety against numerical error. checkAssetBounds(params, derived, invariant, balInNew, ixIn); - amountIn = balInNew.toUint256() -balances[ixIn]; + amountIn = balInNew.toUint256() - balances[ixIn]; } /** @dev Variables are named for calculating y given x @@ -712,6 +716,15 @@ library GyroECLPMath { // note that the error correction in the invariant should more than make up for uncaught rounding directions (in 38 decimals) in virtual offsets Vector2 memory ba = Vector2(virtualOffset1(params, d, r), virtualOffset0(params, d, r)); // change x->y, s->c, c->s, b->a, a->b, tauBeta.x -> -tauAlpha.x, tauBeta.y -> tauAlpha.y vs calcYGivenX - x = solveQuadraticSwap(params.lambda, y, params.c, params.s, r, ba, Vector2(-d.tauAlpha.x, d.tauAlpha.y), d.dSq); + x = solveQuadraticSwap( + params.lambda, + y, + params.c, + params.s, + r, + ba, + Vector2(-d.tauAlpha.x, d.tauAlpha.y), + d.dSq + ); } } diff --git a/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol b/pkg/pool-gyro/test/foundry/E2eBatchSwap2CLP.t.sol similarity index 100% rename from pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol rename to pkg/pool-gyro/test/foundry/E2eBatchSwap2CLP.t.sol diff --git a/pkg/pool-gyro/test/foundry/E2eSwap.t.sol b/pkg/pool-gyro/test/foundry/E2eSwap2CLP.t.sol similarity index 100% rename from pkg/pool-gyro/test/foundry/E2eSwap.t.sol rename to pkg/pool-gyro/test/foundry/E2eSwap2CLP.t.sol diff --git a/pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapRateProvider2CLP.t.sol similarity index 100% rename from pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol rename to pkg/pool-gyro/test/foundry/E2eSwapRateProvider2CLP.t.sol diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximation2CLP.t.sol similarity index 100% rename from pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol rename to pkg/pool-gyro/test/foundry/LiquidityApproximation2CLP.t.sol diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol new file mode 100644 index 000000000..19fd19a87 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { PoolRoleAccounts } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; + +import { LiquidityApproximationTest } from "@balancer-labs/v3-vault/test/foundry/LiquidityApproximation.t.sol"; + +import { GyroECLPPoolFactory } from "../../contracts/GyroECLPPoolFactory.sol"; +import { GyroECLPPool } from "../../contracts/GyroECLPPool.sol"; +import { GyroECLPMath } from "../../contracts/lib/GyroECLPMath.sol"; + +contract LiquidityApproximationECLPTest is LiquidityApproximationTest { + using CastingHelpers for address[]; + + uint256 poolCreationNonce; + + // Extracted from pool 0x2191df821c198600499aa1f0031b1a7514d7a7d9 on Mainnet. + int256 internal _paramsAlpha = 998502246630054917; + int256 internal _paramsBeta = 1000200040008001600; + int256 internal _paramsC = 707106781186547524; + int256 internal _paramsS = 707106781186547524; + int256 internal _paramsLambda = 4000000000000000000000; + + int256 internal _tauAlphaX = -94861212813096057289512505574275160547; + int256 internal _tauAlphaY = 31644119574235279926451292677567331630; + int256 internal _tauBetaX = 37142269533113549537591131345643981951; + int256 internal _tauBetaY = 92846388265400743995957747409218517601; + int256 internal _u = 66001741173104803338721745994955553010; + int256 internal _v = 62245253919818011890633399060291020887; + int256 internal _w = 30601134345582732000058913853921008022; + int256 internal _z = -28859471639991253843240999485797747790; + int256 internal _dSq = 99999999999999999886624093342106115200; + + function setUp() public virtual override { + LiquidityApproximationTest.setUp(); + + // The invariant of ECLP pools are smaller. + maxAmount = 3e5 * 1e18; + } + + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + GyroECLPPoolFactory factory = new GyroECLPPoolFactory(IVault(address(vault)), 365 days); + + PoolRoleAccounts memory roleAccounts; + + GyroECLPMath.Params memory params = GyroECLPMath.Params({ + alpha: _paramsAlpha, + beta: _paramsBeta, + c: _paramsC, + s: _paramsS, + lambda: _paramsLambda + }); + + GyroECLPMath.DerivedParams memory derivedParams = GyroECLPMath.DerivedParams({ + tauAlpha: GyroECLPMath.Vector2(_tauAlphaX, _tauAlphaY), + tauBeta: GyroECLPMath.Vector2(_tauBetaX, _tauBetaY), + u: _u, + v: _v, + w: _w, + z: _z, + dSq: _dSq + }); + + GyroECLPPool newPool = GyroECLPPool( + factory.create( + "Gyro ECLP Pool", + "GRP", + vault.buildTokenConfig(tokens.asIERC20()), + params, + derivedParams, + roleAccounts, + 0, + address(0), + ZERO_BYTES32 + ) + ); + vm.label(address(newPool), label); + return address(newPool); + } +} From 50cb3fecf3273629c022a4ef0aafd53690eb05bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 13:11:32 -0300 Subject: [PATCH 14/52] Fix LiquidityApproximation tests --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 2 +- pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol | 2 +- pkg/vault/test/foundry/LiquidityApproximation.t.sol | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index feb9514ce..1983a59b2 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -102,7 +102,7 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { if (rounding == Rounding.ROUND_DOWN) { return currentInvariant.toUint256(); } else { - return (currentInvariant + 2 * invErr).toUint256(); + return (currentInvariant + 20 * invErr).toUint256(); } } diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol index 19fd19a87..924d8a6aa 100644 --- a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol @@ -41,7 +41,7 @@ contract LiquidityApproximationECLPTest is LiquidityApproximationTest { LiquidityApproximationTest.setUp(); // The invariant of ECLP pools are smaller. - maxAmount = 3e5 * 1e18; + maxAmount = 1e6 * 1e18; } function _createPool(address[] memory tokens, string memory label) internal override returns (address) { diff --git a/pkg/vault/test/foundry/LiquidityApproximation.t.sol b/pkg/vault/test/foundry/LiquidityApproximation.t.sol index fdcf4d4fc..3ee61b66c 100644 --- a/pkg/vault/test/foundry/LiquidityApproximation.t.sol +++ b/pkg/vault/test/foundry/LiquidityApproximation.t.sol @@ -545,9 +545,9 @@ contract LiquidityApproximationTest is BaseVaultTest { _setSwapFeePercentage(address(liquidityPool), swapFeePercentage); _setSwapFeePercentage(address(swapPool), swapFeePercentage); - // Add liquidity so we have something to remove. uint256 currentTotalSupply = IERC20(liquidityPool).totalSupply(); - vm.prank(alice); + vm.startPrank(alice); + // Add liquidity so we have something to remove. router.addLiquidityProportional( address(liquidityPool), [MAX_UINT128, MAX_UINT128].toMemoryArray(), @@ -556,7 +556,6 @@ contract LiquidityApproximationTest is BaseVaultTest { bytes("") ); - vm.startPrank(alice); router.removeLiquiditySingleTokenExactOut( address(liquidityPool), IERC20(liquidityPool).balanceOf(alice), From 801be0cb3978bacb4e55a8af43afc41387d92aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 15:06:49 -0300 Subject: [PATCH 15/52] Create EclpPoolDeployer, to reuse the same pool creation for multiple tests --- pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol | 80 +++++++++++++++ .../foundry/E2eSwapRateProviderECLP.t.sol | 82 +++++++++++++++ .../foundry/LiquidityApproximationECLP.t.sol | 71 +------------ .../test/foundry/utils/EclpPoolDeployer.sol | 99 +++++++++++++++++++ 4 files changed, 266 insertions(+), 66 deletions(-) create mode 100644 pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol create mode 100644 pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol create mode 100644 pkg/pool-gyro/test/foundry/utils/EclpPoolDeployer.sol diff --git a/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol new file mode 100644 index 000000000..1a755ef84 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + +import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; +import { E2eSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwap.t.sol"; + +import { EclpPoolDeployer } from "./utils/EclpPoolDeployer.sol"; + +contract E2eSwapECLPTest is E2eSwapTest, EclpPoolDeployer { + using FixedPoint for uint256; + + function setUp() public override { + E2eSwapTest.setUp(); + } + + /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eSwapTest tests. + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); + return createEclpPool(tokens, rateProviders, label, vault, lp); + } + + function setUpVariables() internal override { + sender = lp; + poolCreator = lp; + + // 0.0001% min swap fee. + minPoolSwapFeePercentage = 1e12; + // 10% max swap fee. + maxPoolSwapFeePercentage = 10e16; + } + + function calculateMinAndMaxSwapAmounts() internal virtual override { + uint256 rateTokenA = getRate(tokenA); + uint256 rateTokenB = getRate(tokenB); + + // The vault does not allow trade amounts (amountGivenScaled18 or amountCalculatedScaled18) to be less than + // MIN_TRADE_AMOUNT. For "linear pools" (PoolMock), amountGivenScaled18 and amountCalculatedScaled18 are + // the same. So, minAmountGivenScaled18 > MIN_TRADE_AMOUNT. To derive the formula below, note that + // `amountGivenRaw = amountGivenScaled18/(rateToken * scalingFactor)`. There's an adjustment for stable math + // in the following steps. + uint256 tokenAMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenA).mulUp(10 ** decimalsTokenA); + uint256 tokenBMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenB).mulUp(10 ** decimalsTokenB); + + // Also, since we undo the operation (reverse swap with the output of the first swap), amountCalculatedRaw + // cannot be 0. Considering that amountCalculated is tokenB, and amountGiven is tokenA: + // 1) amountCalculatedRaw > 0 + // 2) amountCalculatedRaw = amountCalculatedScaled18 * 10^(decimalsB) / (rateB * 10^18) + // 3) amountCalculatedScaled18 = amountGivenScaled18 // Linear math, there's a factor to stable math + // 4) amountGivenScaled18 = amountGivenRaw * rateA * 10^18 / 10^(decimalsA) + // Using the four formulas above, we determine that: + // amountCalculatedRaw > rateB * 10^(decimalsA) / (rateA * 10^(decimalsB)) + uint256 tokenACalculatedNotZero = (rateTokenB * (10 ** decimalsTokenA)) / (rateTokenA * (10 ** decimalsTokenB)); + uint256 tokenBCalculatedNotZero = (rateTokenA * (10 ** decimalsTokenB)) / (rateTokenB * (10 ** decimalsTokenA)); + + // Use the larger of the two values above to calculate the minSwapAmount. Also, multiply by 10 to account for + // swap fees and compensate for rate rounding issues. + uint256 mathFactor = 10; + minSwapAmountTokenA = ( + tokenAMinTradeAmount > tokenACalculatedNotZero + ? mathFactor * tokenAMinTradeAmount + : mathFactor * tokenACalculatedNotZero + ); + minSwapAmountTokenB = ( + tokenBMinTradeAmount > tokenBCalculatedNotZero + ? mathFactor * tokenBMinTradeAmount + : mathFactor * tokenBCalculatedNotZero + ); + + // 50% of pool init amount to make sure LP has enough tokens to pay for the swap in case of EXACT_OUT. + maxSwapAmountTokenA = poolInitAmountTokenA.mulDown(50e16); + maxSwapAmountTokenB = poolInitAmountTokenB.mulDown(50e16); + } +} diff --git a/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol new file mode 100644 index 000000000..37aa0aa04 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + +import { PoolHooksMock } from "@balancer-labs/v3-vault/contracts/test/PoolHooksMock.sol"; +import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; +import { RateProviderMock } from "@balancer-labs/v3-vault/contracts/test/RateProviderMock.sol"; +import { E2eSwapRateProviderTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwapRateProvider.t.sol"; +import { VaultContractsDeployer } from "@balancer-labs/v3-vault/test/foundry/utils/VaultContractsDeployer.sol"; + +import { EclpPoolDeployer } from "./utils/EclpPoolDeployer.sol"; + +contract E2eSwapRateProviderECLPTest is VaultContractsDeployer, E2eSwapRateProviderTest, EclpPoolDeployer { + using CastingHelpers for address[]; + using FixedPoint for uint256; + + /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eSwapTest tests. + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + rateProviderTokenA = deployRateProviderMock(); + rateProviderTokenB = deployRateProviderMock(); + // Mock rates, so all tests that keep the rate constant use a rate different than 1. + rateProviderTokenA.mockRate(5.2453235e18); + rateProviderTokenB.mockRate(0.4362784e18); + + IRateProvider[] memory rateProviders = new IRateProvider[](2); + rateProviders[tokenAIdx] = IRateProvider(address(rateProviderTokenA)); + rateProviders[tokenBIdx] = IRateProvider(address(rateProviderTokenB)); + + return createEclpPool(tokens, rateProviders, label, vault, lp); + } + + function calculateMinAndMaxSwapAmounts() internal virtual override { + uint256 rateTokenA = getRate(tokenA); + uint256 rateTokenB = getRate(tokenB); + + // The vault does not allow trade amounts (amountGivenScaled18 or amountCalculatedScaled18) to be less than + // PRODUCTION_MIN_TRADE_AMOUNT. For "linear pools" (PoolMock), amountGivenScaled18 and amountCalculatedScaled18 + // are the same. So, minAmountGivenScaled18 > PRODUCTION_MIN_TRADE_AMOUNT. To derive the formula below, note + // that `amountGivenRaw = amountGivenScaled18/(rateToken * scalingFactor)`. There's an adjustment for stable + // math in the following steps. + uint256 tokenAMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenA).mulUp(10 ** decimalsTokenA); + uint256 tokenBMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenB).mulUp(10 ** decimalsTokenB); + + // Also, since we undo the operation (reverse swap with the output of the first swap), amountCalculatedRaw + // cannot be 0. Considering that amountCalculated is tokenB, and amountGiven is tokenA: + // 1) amountCalculatedRaw > 0 + // 2) amountCalculatedRaw = amountCalculatedScaled18 * 10^(decimalsB) / (rateB * 10^18) + // 3) amountCalculatedScaled18 = amountGivenScaled18 // Linear math, there's a factor to stable math + // 4) amountGivenScaled18 = amountGivenRaw * rateA * 10^18 / 10^(decimalsA) + // Combining the four formulas above, we determine that: + // amountCalculatedRaw > rateB * 10^(decimalsA) / (rateA * 10^(decimalsB)) + uint256 tokenACalculatedNotZero = (rateTokenB * (10 ** decimalsTokenA)) / (rateTokenA * (10 ** decimalsTokenB)); + uint256 tokenBCalculatedNotZero = (rateTokenA * (10 ** decimalsTokenB)) / (rateTokenB * (10 ** decimalsTokenA)); + + // Use the larger of the two values above to calculate the minSwapAmount. Also, multiply by 100 to account for + // swap fees and compensate for rate and math rounding issues. + uint256 mathFactor = 100; + minSwapAmountTokenA = ( + tokenAMinTradeAmount > tokenACalculatedNotZero + ? mathFactor * tokenAMinTradeAmount + : mathFactor * tokenACalculatedNotZero + ); + minSwapAmountTokenB = ( + tokenBMinTradeAmount > tokenBCalculatedNotZero + ? mathFactor * tokenBMinTradeAmount + : mathFactor * tokenBCalculatedNotZero + ); + + // 50% of pool init amount to make sure LP has enough tokens to pay for the swap in case of EXACT_OUT. + maxSwapAmountTokenA = poolInitAmountTokenA.mulDown(50e16); + maxSwapAmountTokenB = poolInitAmountTokenB.mulDown(50e16); + } +} diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol index 924d8a6aa..17b06fd5b 100644 --- a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol @@ -4,39 +4,13 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; -import { PoolRoleAccounts } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; - -import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; import { LiquidityApproximationTest } from "@balancer-labs/v3-vault/test/foundry/LiquidityApproximation.t.sol"; -import { GyroECLPPoolFactory } from "../../contracts/GyroECLPPoolFactory.sol"; -import { GyroECLPPool } from "../../contracts/GyroECLPPool.sol"; -import { GyroECLPMath } from "../../contracts/lib/GyroECLPMath.sol"; - -contract LiquidityApproximationECLPTest is LiquidityApproximationTest { - using CastingHelpers for address[]; - - uint256 poolCreationNonce; - - // Extracted from pool 0x2191df821c198600499aa1f0031b1a7514d7a7d9 on Mainnet. - int256 internal _paramsAlpha = 998502246630054917; - int256 internal _paramsBeta = 1000200040008001600; - int256 internal _paramsC = 707106781186547524; - int256 internal _paramsS = 707106781186547524; - int256 internal _paramsLambda = 4000000000000000000000; - - int256 internal _tauAlphaX = -94861212813096057289512505574275160547; - int256 internal _tauAlphaY = 31644119574235279926451292677567331630; - int256 internal _tauBetaX = 37142269533113549537591131345643981951; - int256 internal _tauBetaY = 92846388265400743995957747409218517601; - int256 internal _u = 66001741173104803338721745994955553010; - int256 internal _v = 62245253919818011890633399060291020887; - int256 internal _w = 30601134345582732000058913853921008022; - int256 internal _z = -28859471639991253843240999485797747790; - int256 internal _dSq = 99999999999999999886624093342106115200; +import { EclpPoolDeployer } from "./utils/EclpPoolDeployer.sol"; +contract LiquidityApproximationECLPTest is LiquidityApproximationTest, EclpPoolDeployer { function setUp() public virtual override { LiquidityApproximationTest.setUp(); @@ -45,42 +19,7 @@ contract LiquidityApproximationECLPTest is LiquidityApproximationTest { } function _createPool(address[] memory tokens, string memory label) internal override returns (address) { - GyroECLPPoolFactory factory = new GyroECLPPoolFactory(IVault(address(vault)), 365 days); - - PoolRoleAccounts memory roleAccounts; - - GyroECLPMath.Params memory params = GyroECLPMath.Params({ - alpha: _paramsAlpha, - beta: _paramsBeta, - c: _paramsC, - s: _paramsS, - lambda: _paramsLambda - }); - - GyroECLPMath.DerivedParams memory derivedParams = GyroECLPMath.DerivedParams({ - tauAlpha: GyroECLPMath.Vector2(_tauAlphaX, _tauAlphaY), - tauBeta: GyroECLPMath.Vector2(_tauBetaX, _tauBetaY), - u: _u, - v: _v, - w: _w, - z: _z, - dSq: _dSq - }); - - GyroECLPPool newPool = GyroECLPPool( - factory.create( - "Gyro ECLP Pool", - "GRP", - vault.buildTokenConfig(tokens.asIERC20()), - params, - derivedParams, - roleAccounts, - 0, - address(0), - ZERO_BYTES32 - ) - ); - vm.label(address(newPool), label); - return address(newPool); + IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); + return createEclpPool(tokens, rateProviders, label, vault, lp); } } diff --git a/pkg/pool-gyro/test/foundry/utils/EclpPoolDeployer.sol b/pkg/pool-gyro/test/foundry/utils/EclpPoolDeployer.sol new file mode 100644 index 000000000..82537aaaa --- /dev/null +++ b/pkg/pool-gyro/test/foundry/utils/EclpPoolDeployer.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { PoolRoleAccounts } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import { IVaultMock } from "@balancer-labs/v3-interfaces/contracts/test/IVaultMock.sol"; +import { TokenConfig } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; + +import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; + +import { GyroECLPPoolFactory } from "../../../contracts/GyroECLPPoolFactory.sol"; +import { GyroECLPPool } from "../../../contracts/GyroECLPPool.sol"; +import { GyroECLPMath } from "../../../contracts/lib/GyroECLPMath.sol"; + +contract EclpPoolDeployer is Test { + using CastingHelpers for address[]; + + // Extracted from pool 0x2191df821c198600499aa1f0031b1a7514d7a7d9 on Mainnet. + int256 internal _paramsAlpha = 998502246630054917; + int256 internal _paramsBeta = 1000200040008001600; + int256 internal _paramsC = 707106781186547524; + int256 internal _paramsS = 707106781186547524; + int256 internal _paramsLambda = 4000000000000000000000; + + int256 internal _tauAlphaX = -94861212813096057289512505574275160547; + int256 internal _tauAlphaY = 31644119574235279926451292677567331630; + int256 internal _tauBetaX = 37142269533113549537591131345643981951; + int256 internal _tauBetaY = 92846388265400743995957747409218517601; + int256 internal _u = 66001741173104803338721745994955553010; + int256 internal _v = 62245253919818011890633399060291020887; + int256 internal _w = 30601134345582732000058913853921008022; + int256 internal _z = -28859471639991253843240999485797747790; + int256 internal _dSq = 99999999999999999886624093342106115200; + + function createEclpPool( + address[] memory tokens, + IRateProvider[] memory rateProviders, + string memory label, + IVaultMock vault, + address poolCreator + ) internal returns (address) { + GyroECLPPoolFactory factory = new GyroECLPPoolFactory(IVault(address(vault)), 365 days); + + PoolRoleAccounts memory roleAccounts; + GyroECLPPool newPool; + + // Avoids Stack-too-deep. + { + GyroECLPMath.Params memory params = GyroECLPMath.Params({ + alpha: _paramsAlpha, + beta: _paramsBeta, + c: _paramsC, + s: _paramsS, + lambda: _paramsLambda + }); + + GyroECLPMath.DerivedParams memory derivedParams = GyroECLPMath.DerivedParams({ + tauAlpha: GyroECLPMath.Vector2(_tauAlphaX, _tauAlphaY), + tauBeta: GyroECLPMath.Vector2(_tauBetaX, _tauBetaY), + u: _u, + v: _v, + w: _w, + z: _z, + dSq: _dSq + }); + + TokenConfig[] memory tokenConfig = vault.buildTokenConfig(tokens.asIERC20(), rateProviders); + + newPool = GyroECLPPool( + factory.create( + label, + label, + tokenConfig, + params, + derivedParams, + roleAccounts, + 0, + address(0), + bytes32("") + ) + ); + } + vm.label(address(newPool), label); + + // Cannot set the pool creator directly on a standard Balancer stable pool factory. + vault.manualSetPoolCreator(address(newPool), poolCreator); + + ProtocolFeeControllerMock feeController = ProtocolFeeControllerMock(address(vault.getProtocolFeeController())); + feeController.manualSetPoolCreator(address(newPool), poolCreator); + + return address(newPool); + } +} From a1a95eb67d7603359a1ac73630bcec6783c7adea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 15:11:20 -0300 Subject: [PATCH 16/52] Fix E2E tests --- .../test/foundry/E2eBatchSwapECLP.t.sol | 40 +++++++++++++++++++ pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol | 1 - .../foundry/E2eSwapRateProviderECLP.t.sol | 6 --- 3 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol diff --git a/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol new file mode 100644 index 000000000..3e21530d2 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; + +import { ERC20TestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/ERC20TestToken.sol"; + +import { E2eBatchSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eBatchSwap.t.sol"; + +import { EclpPoolDeployer } from "./utils/EclpPoolDeployer.sol"; + +contract E2eBatchSwapECLPTest is E2eBatchSwapTest, EclpPoolDeployer { + /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eBatchSwapTest tests. + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); + return createEclpPool(tokens, rateProviders, label, vault, lp); + } + + function _setUpVariables() internal override { + tokenA = dai; + tokenB = usdc; + tokenC = ERC20TestToken(address(weth)); + tokenD = wsteth; + sender = lp; + poolCreator = lp; + + // If there are swap fees, the amountCalculated may be lower than MIN_TRADE_AMOUNT. So, multiplying + // MIN_TRADE_AMOUNT by 10 creates a margin. + minSwapAmountTokenA = 10 * PRODUCTION_MIN_TRADE_AMOUNT; + minSwapAmountTokenD = 10 * PRODUCTION_MIN_TRADE_AMOUNT; + + // Divide init amount by 10 to make sure weighted math ratios are respected (Cannot trade more than 30% of pool + // balance). + maxSwapAmountTokenA = poolInitAmount / 10; + maxSwapAmountTokenD = poolInitAmount / 10; + } +} diff --git a/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol index 1a755ef84..4ec77c290 100644 --- a/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol @@ -8,7 +8,6 @@ import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-u import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; -import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; import { E2eSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwap.t.sol"; import { EclpPoolDeployer } from "./utils/EclpPoolDeployer.sol"; diff --git a/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol index 37aa0aa04..a1b57bb38 100644 --- a/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol @@ -5,14 +5,9 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; -import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; -import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; -import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; -import { PoolHooksMock } from "@balancer-labs/v3-vault/contracts/test/PoolHooksMock.sol"; -import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; import { RateProviderMock } from "@balancer-labs/v3-vault/contracts/test/RateProviderMock.sol"; import { E2eSwapRateProviderTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwapRateProvider.t.sol"; import { VaultContractsDeployer } from "@balancer-labs/v3-vault/test/foundry/utils/VaultContractsDeployer.sol"; @@ -20,7 +15,6 @@ import { VaultContractsDeployer } from "@balancer-labs/v3-vault/test/foundry/uti import { EclpPoolDeployer } from "./utils/EclpPoolDeployer.sol"; contract E2eSwapRateProviderECLPTest is VaultContractsDeployer, E2eSwapRateProviderTest, EclpPoolDeployer { - using CastingHelpers for address[]; using FixedPoint for uint256; /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eSwapTest tests. From 6f0dd1549b8e31a6ce790cfcf178199637ac168b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 15:28:03 -0300 Subject: [PATCH 17/52] Fix comments --- .../contracts/lib/SignedFixedPoint.sol | 118 +++++++++++------- 1 file changed, 71 insertions(+), 47 deletions(-) diff --git a/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol b/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol index 0a5fd72bf..9680f56a0 100644 --- a/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol +++ b/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol @@ -1,14 +1,17 @@ // SPDX-License-Identifier: LicenseRef-Gyro-1.0 -// for information on licensing please see the README in the GitHub repository . +// for information on licensing please see the README in the GitHub repository +// . pragma solidity ^0.8.24; /* solhint-disable private-vars-leading-underscore */ -/// @dev Signed fixed point operations based on Balancer's FixedPoint library. -/// Note: The `{mul,div}{UpMag,DownMag}()` functions do *not* round up or down, respectively, -/// in a signed fashion (like ceil and floor operations), but *in absolute value* (or *magnitude*), i.e., -/// towards 0. This is useful in some applications. +/** + * @notice Signed fixed point operations based on Balancer's FixedPoint library. + * @dev The `{mul,div}{UpMag,DownMag}()` functions do *not* round up or down, respectively, in a signed fashion (like + * ceil and floor operations), but *in absolute value* (or *magnitude*), i.e., towards 0. This is useful in some + * applications. + */ library SignedFixedPoint { error AddOverflow(); error SubOverflow(); @@ -17,8 +20,8 @@ library SignedFixedPoint { error DivInterval(); int256 internal constant ONE = 1e18; // 18 decimal places - // setting extra precision at 38 decimals, which is the most we can get w/o overflowing on normal multiplication - // this allows 20 extra digits to absorb error when multiplying by large numbers + // Setting extra precision at 38 decimals, which is the most we can get without overflowing on normal + // multiplication. This allows 20 extra digits to absorb error when multiplying by large numbers. int256 internal constant ONE_XP = 1e38; // 38 decimal places function add(int256 a, int256 b) internal pure returns (int256) { @@ -50,7 +53,10 @@ library SignedFixedPoint { return product / ONE; } - /// @dev this implements mulDownMag w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + /** + * @dev This implements mulDownMag without checking for over/under-flows, which saves significantly on gas if these + * aren't needed + */ function mulDownMagU(int256 a, int256 b) internal pure returns (int256) { return (a * b) / ONE; } @@ -69,7 +75,10 @@ library SignedFixedPoint { return 0; } - /// @dev this implements mulUpMag w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + /** + * @dev this implements mulDownMag without checking for over/under-flows, which saves significantly on gas if these + * aren't needed + */ function mulUpMagU(int256 a, int256 b) internal pure returns (int256) { int256 product = a * b; @@ -96,7 +105,10 @@ library SignedFixedPoint { return aInflated / b; } - /// @dev this implements divDownMag w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + /** + * @dev this implements mulDownMag without checking for over/under-flows, which saves significantly on gas if these + * aren't needed + */ function divDownMagU(int256 a, int256 b) internal pure returns (int256) { if (b == 0) revert ZeroDivision(); return (a * ONE) / b; @@ -123,7 +135,10 @@ library SignedFixedPoint { return ((aInflated + 1) / b) - 1; } - /// @dev this implements divUpMag w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + /** + * @dev this implements mulDownMag without checking for over/under-flows, which saves significantly on gas if these + * aren't needed + */ function divUpMagU(int256 a, int256 b) internal pure returns (int256) { if (b == 0) revert ZeroDivision(); @@ -131,7 +146,8 @@ library SignedFixedPoint { return 0; } - // SOMEDAY check if we can shave off some gas by logically refactoring this vs the below case distinction into one (on a * b or so). + // SOMEDAY check if we can shave off some gas by logically refactoring this vs the below case distinction + // into one (on a * b or so). if (b < 0) { // Ensure b > 0 so the below is correct. b = -b; @@ -142,9 +158,11 @@ library SignedFixedPoint { return ((a * ONE + 1) / b) - 1; } - /// @dev multiplies two extra precision numbers (with 38 decimals) - /// rounds down in magnitude but this shouldn't matter - /// multiplication can overflow if a,b are > 2 in magnitude + /** + * @notice Multiplies two extra precision numbers (with 38 decimals). + * @dev Rounds down in magnitude but this shouldn't matter. Multiplication can overflow if a,b are > 2 in + * magnitude. + */ function mulXp(int256 a, int256 b) internal pure returns (int256) { int256 product = a * b; if (!(a == 0 || product / a == b)) revert MulOverflow(); @@ -152,17 +170,20 @@ library SignedFixedPoint { return product / ONE_XP; } - /// @dev multiplies two extra precision numbers (with 38 decimals) - /// rounds down in magnitude but this shouldn't matter - /// multiplication can overflow if a,b are > 2 in magnitude - /// this implements mulXp w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + /** + * @notice Multiplies two extra precision numbers (with 38 decimals). + * @dev Rounds down in magnitude but this shouldn't matter. Multiplication can overflow if a,b are > 2 in + * magnitude. This implements mulXp without checking for over/under-flows, which saves significantly on gas if + * these aren't needed. + */ function mulXpU(int256 a, int256 b) internal pure returns (int256) { return (a * b) / ONE_XP; } - /// @dev divides two extra precision numbers (with 38 decimals) - /// rounds down in magnitude but this shouldn't matter - /// can overflow if a > 2 or b << 1 in magnitude + /** + * @notice @notice Divides two extra precision numbers (with 38 decimals). + * @dev Rounds down in magnitude but this shouldn't matter. Division can overflow if a > 2 or b << 1 in magnitude. + */ function divXp(int256 a, int256 b) internal pure returns (int256) { if (b == 0) revert ZeroDivision(); @@ -176,19 +197,22 @@ library SignedFixedPoint { return aInflated / b; } - /// @dev divides two extra precision numbers (with 38 decimals) - /// rounds down in magnitude but this shouldn't matter - /// can overflow if a > 2 or b << 1 in magnitude - /// this implements divXp w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + /** + * @notice Divides two extra precision numbers (with 38 decimals). + * @dev Rounds down in magnitude but this shouldn't matter. Division can overflow if a > 2 or b << 1 in magnitude. + * This implements divXp without checking for over/under-flows, which saves significantly on gas if these aren't + * needed. + */ function divXpU(int256 a, int256 b) internal pure returns (int256) { if (b == 0) revert ZeroDivision(); return (a * ONE_XP) / b; } - /// @dev multiplies normal precision a with extra precision b (with 38 decimals) - /// Rounds down in signed direction - /// returns normal precision of the product + /** + * @notice Multiplies normal precision a with extra precision b (with 38 decimals). + * @dev Rounds down in signed direction. Returns normal precision of the product. + */ function mulDownXpToNp(int256 a, int256 b) internal pure returns (int256) { int256 b1 = b / 1e19; int256 prod1 = a * b1; @@ -199,10 +223,11 @@ library SignedFixedPoint { return prod1 >= 0 && prod2 >= 0 ? (prod1 + prod2 / 1e19) / 1e19 : (prod1 + prod2 / 1e19 + 1) / 1e19 - 1; } - /// @dev multiplies normal precision a with extra precision b (with 38 decimals) - /// Rounds down in signed direction - /// returns normal precision of the product - /// this implements mulDownXpToNp w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + /** + * @notice Multiplies normal precision a with extra precision b (with 38 decimals). + * @dev Rounds down in signed direction. Returns normal precision of the product. This implements mulDownXpToNp + * without checking for over/under-flows, which saves significantly on gas if these aren't needed. + */ function mulDownXpToNpU(int256 a, int256 b) internal pure returns (int256) { int256 b1 = b / 1e19; int256 b2 = b % 1e19; @@ -212,9 +237,10 @@ library SignedFixedPoint { return prod1 >= 0 && prod2 >= 0 ? (prod1 + prod2 / 1e19) / 1e19 : (prod1 + prod2 / 1e19 + 1) / 1e19 - 1; } - /// @dev multiplies normal precision a with extra precision b (with 38 decimals) - /// Rounds up in signed direction - /// returns normal precision of the product + /** + * @notice Multiplies normal precision a with extra precision b (with 38 decimals). + * @dev Rounds down in signed direction. Returns normal precision of the product. + */ function mulUpXpToNp(int256 a, int256 b) internal pure returns (int256) { int256 b1 = b / 1e19; int256 prod1 = a * b1; @@ -225,26 +251,24 @@ library SignedFixedPoint { return prod1 <= 0 && prod2 <= 0 ? (prod1 + prod2 / 1e19) / 1e19 : (prod1 + prod2 / 1e19 - 1) / 1e19 + 1; } - /// @dev multiplies normal precision a with extra precision b (with 38 decimals) - /// Rounds up in signed direction - /// returns normal precision of the product - /// this implements mulUpXpToNp w/o checking for over/under-flows, which saves significantly on gas if these aren't needed + /** + * @notice Multiplies normal precision a with extra precision b (with 38 decimals). + * @dev Rounds down in signed direction. Returns normal precision of the product. This implements mulUpXpToNp + * without checking for over/under-flows, which saves significantly on gas if these aren't needed. + */ function mulUpXpToNpU(int256 a, int256 b) internal pure returns (int256) { int256 b1 = b / 1e19; int256 b2 = b % 1e19; - // SOMEDAY check if we eliminate these vars and save some gas (by only checking the sign of prod1, say) + // SOMEDAY check if we eliminate these vars and save some gas (by only checking the sign of prod1, say). int256 prod1 = a * b1; int256 prod2 = a * b2; return prod1 <= 0 && prod2 <= 0 ? (prod1 + prod2 / 1e19) / 1e19 : (prod1 + prod2 / 1e19 - 1) / 1e19 + 1; } - // not implementing the pow functions right now b/c it's annoying and slightly ill-defined, and we don't use them. - /** - * @dev Returns the complement of a value (1 - x), capped to 0 if x is larger than 1. - * - * Useful when computing the complement for values with some level of relative error, as it strips this error and - * prevents intermediate negative values. + * @notice Returns the complement of a value (1 - x), capped to 0 if x is larger than 1. + * @dev Useful when computing the complement for values with some level of relative error, as it strips this + * error and prevents intermediate negative values. */ function complement(int256 x) internal pure returns (int256) { if (x >= ONE || x <= 0) return 0; From f5fabda914a19e89fda7115dce37af3b51806e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 15:46:39 -0300 Subject: [PATCH 18/52] Implement deployer to 2CLP --- ...Balance.t.sol => ComputeBalance2CLP.t.sol} | 2 +- pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol | 51 +++------------- pkg/pool-gyro/test/foundry/E2eSwap.t.sol | 53 +++-------------- .../test/foundry/E2eSwapRateProvider.t.sol | 43 +------------- .../foundry/LiquidityApproximationGyro.t.sol | 39 ++---------- .../foundry/utils/Gyro2ClpPoolDeployer.sol | 59 +++++++++++++++++++ 6 files changed, 85 insertions(+), 162 deletions(-) rename pkg/pool-gyro/test/foundry/{ComputeBalance.t.sol => ComputeBalance2CLP.t.sol} (98%) create mode 100644 pkg/pool-gyro/test/foundry/utils/Gyro2ClpPoolDeployer.sol diff --git a/pkg/pool-gyro/test/foundry/ComputeBalance.t.sol b/pkg/pool-gyro/test/foundry/ComputeBalance2CLP.t.sol similarity index 98% rename from pkg/pool-gyro/test/foundry/ComputeBalance.t.sol rename to pkg/pool-gyro/test/foundry/ComputeBalance2CLP.t.sol index ac7d6d0d6..f091468e2 100644 --- a/pkg/pool-gyro/test/foundry/ComputeBalance.t.sol +++ b/pkg/pool-gyro/test/foundry/ComputeBalance2CLP.t.sol @@ -12,7 +12,7 @@ import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVa import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; -contract ComputeBalanceTest is BaseVaultTest { +contract ComputeBalance2CLPTest is BaseVaultTest { using FixedPoint for uint256; Gyro2CLPPool private _gyroPool; diff --git a/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol b/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol index 2a7658ae6..e89eaf910 100644 --- a/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol @@ -4,25 +4,20 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; -import { PoolRoleAccounts } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; -import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; import { ERC20TestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/ERC20TestToken.sol"; -import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; import { E2eBatchSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eBatchSwap.t.sol"; -import { Gyro2CLPPoolFactory } from "../../contracts/Gyro2CLPPoolFactory.sol"; -import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; +import { Gyro2ClpPoolDeployer } from "./utils/Gyro2ClpPoolDeployer.sol"; -contract E2eBatchSwapGyro2CLPTest is E2eBatchSwapTest { - using CastingHelpers for address[]; - - uint256 poolCreationNonce; - - uint256 private _sqrtAlpha = 997496867163000167; // alpha (lower price rate) = 0.995 - uint256 private _sqrtBeta = 1002496882788171068; // beta (upper price rate) = 1.005 +contract E2eBatchSwapGyro2CLPTest is E2eBatchSwapTest, Gyro2ClpPoolDeployer { + /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eBatchSwapTest tests. + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); + return createGyro2ClpPool(tokens, rateProviders, label, vault, lp); + } function _setUpVariables() internal override { tokenA = dai; @@ -42,34 +37,4 @@ contract E2eBatchSwapGyro2CLPTest is E2eBatchSwapTest { maxSwapAmountTokenA = poolInitAmount / 10; maxSwapAmountTokenD = poolInitAmount / 10; } - - /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eBatchSwapTest tests. - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { - Gyro2CLPPoolFactory factory = new Gyro2CLPPoolFactory(IVault(address(vault)), 365 days); - - PoolRoleAccounts memory roleAccounts; - - Gyro2CLPPool newPool = Gyro2CLPPool( - factory.create( - label, - label, - vault.buildTokenConfig(tokens.asIERC20()), - _sqrtAlpha, - _sqrtBeta, - roleAccounts, - 0, - address(0), - ZERO_BYTES32 - ) - ); - vm.label(address(newPool), label); - - // Cannot set the pool creator directly on a standard Balancer stable pool factory. - vault.manualSetPoolCreator(address(newPool), lp); - - ProtocolFeeControllerMock feeController = ProtocolFeeControllerMock(address(vault.getProtocolFeeController())); - feeController.manualSetPoolCreator(address(newPool), lp); - - return address(newPool); - } } diff --git a/pkg/pool-gyro/test/foundry/E2eSwap.t.sol b/pkg/pool-gyro/test/foundry/E2eSwap.t.sol index 6d0b12fb8..35b3c03a2 100644 --- a/pkg/pool-gyro/test/foundry/E2eSwap.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eSwap.t.sol @@ -4,32 +4,27 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; -import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; -import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; -import { PoolHooksMock } from "@balancer-labs/v3-vault/contracts/test/PoolHooksMock.sol"; -import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; import { E2eSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwap.t.sol"; -import { Gyro2CLPPoolFactory } from "../../contracts/Gyro2CLPPoolFactory.sol"; -import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; +import { Gyro2ClpPoolDeployer } from "./utils/Gyro2ClpPoolDeployer.sol"; -contract E2eSwapGyro2CLPTest is E2eSwapTest { - using CastingHelpers for address[]; +contract E2eSwapGyro2CLPTest is E2eSwapTest, Gyro2ClpPoolDeployer { using FixedPoint for uint256; - uint256 poolCreationNonce; - - uint256 private _sqrtAlpha = 997496867163000167; // alpha (lower price rate) = 0.995 - uint256 private _sqrtBeta = 1002496882788171068; // beta (upper price rate) = 1.005 - function setUp() public override { E2eSwapTest.setUp(); } + /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eSwapTest tests. + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); + return createGyro2ClpPool(tokens, rateProviders, label, vault, lp); + } + function setUpVariables() internal override { sender = lp; poolCreator = lp; @@ -81,34 +76,4 @@ contract E2eSwapGyro2CLPTest is E2eSwapTest { maxSwapAmountTokenA = poolInitAmountTokenA.mulDown(50e16); maxSwapAmountTokenB = poolInitAmountTokenB.mulDown(50e16); } - - /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eSwapTest tests. - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { - Gyro2CLPPoolFactory factory = new Gyro2CLPPoolFactory(IVault(address(vault)), 365 days); - - PoolRoleAccounts memory roleAccounts; - - Gyro2CLPPool newPool = Gyro2CLPPool( - factory.create( - "Gyro 2CLP Pool", - "GRP", - vault.buildTokenConfig(tokens.asIERC20()), - _sqrtAlpha, - _sqrtBeta, - roleAccounts, - 0, - address(0), - ZERO_BYTES32 - ) - ); - vm.label(address(newPool), label); - - // Cannot set the pool creator directly on a standard Balancer stable pool factory. - vault.manualSetPoolCreator(address(newPool), lp); - - ProtocolFeeControllerMock feeController = ProtocolFeeControllerMock(address(vault.getProtocolFeeController())); - feeController.manualSetPoolCreator(address(newPool), lp); - - return address(newPool); - } } diff --git a/pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol index 1d8b15054..c31253414 100644 --- a/pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol @@ -5,30 +5,18 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; -import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; -import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; -import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; -import { PoolHooksMock } from "@balancer-labs/v3-vault/contracts/test/PoolHooksMock.sol"; -import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; import { RateProviderMock } from "@balancer-labs/v3-vault/contracts/test/RateProviderMock.sol"; import { E2eSwapRateProviderTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwapRateProvider.t.sol"; import { VaultContractsDeployer } from "@balancer-labs/v3-vault/test/foundry/utils/VaultContractsDeployer.sol"; -import { Gyro2CLPPoolFactory } from "../../contracts/Gyro2CLPPoolFactory.sol"; -import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; +import { Gyro2ClpPoolDeployer } from "./utils/Gyro2ClpPoolDeployer.sol"; -contract E2eSwapRateProviderGyro2CLPTest is VaultContractsDeployer, E2eSwapRateProviderTest { - using CastingHelpers for address[]; +contract E2eSwapRateProviderGyro2CLPTest is VaultContractsDeployer, E2eSwapRateProviderTest, Gyro2ClpPoolDeployer { using FixedPoint for uint256; - uint256 poolCreationNonce; - - uint256 private _sqrtAlpha = 997496867163000167; // alpha (lower price rate) = 0.995 - uint256 private _sqrtBeta = 1002496882788171068; // beta (upper price rate) = 1.005 - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { rateProviderTokenA = deployRateProviderMock(); rateProviderTokenB = deployRateProviderMock(); @@ -40,32 +28,7 @@ contract E2eSwapRateProviderGyro2CLPTest is VaultContractsDeployer, E2eSwapRateP rateProviders[tokenAIdx] = IRateProvider(address(rateProviderTokenA)); rateProviders[tokenBIdx] = IRateProvider(address(rateProviderTokenB)); - Gyro2CLPPoolFactory factory = new Gyro2CLPPoolFactory(IVault(address(vault)), 365 days); - - PoolRoleAccounts memory roleAccounts; - - Gyro2CLPPool newPool = Gyro2CLPPool( - factory.create( - "Gyro 2CLP Pool", - "GRP", - vault.buildTokenConfig(tokens.asIERC20(), rateProviders), - _sqrtAlpha, - _sqrtBeta, - roleAccounts, - 0, - address(0), - ZERO_BYTES32 - ) - ); - vm.label(address(newPool), label); - - // Cannot set the pool creator directly on a standard Balancer stable pool factory. - vault.manualSetPoolCreator(address(newPool), lp); - - ProtocolFeeControllerMock feeController = ProtocolFeeControllerMock(address(vault.getProtocolFeeController())); - feeController.manualSetPoolCreator(address(newPool), lp); - - return address(newPool); + return createGyro2ClpPool(tokens, rateProviders, label, vault, lp); } function calculateMinAndMaxSwapAmounts() internal virtual override { diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol index fb9c11c12..437ade812 100644 --- a/pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol +++ b/pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol @@ -4,48 +4,19 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; -import { PoolRoleAccounts } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; - -import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; - -import { Gyro2CLPPoolFactory } from "../../contracts/Gyro2CLPPoolFactory.sol"; -import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; import { LiquidityApproximationTest } from "@balancer-labs/v3-vault/test/foundry/LiquidityApproximation.t.sol"; -import { Gyro2CLPPool } from "../../contracts/Gyro2CLPPool.sol"; - -contract LiquidityApproximationGyroTest is LiquidityApproximationTest { - using CastingHelpers for address[]; - uint256 poolCreationNonce; - - uint256 private _sqrtAlpha = 997496867163000167; // alpha (lower price rate) = 0.995 - uint256 private _sqrtBeta = 1002496882788171068; // beta (upper price rate) = 1.005 +import { Gyro2ClpPoolDeployer } from "./utils/Gyro2ClpPoolDeployer.sol"; +contract LiquidityApproximationGyroTest is LiquidityApproximationTest, Gyro2ClpPoolDeployer { function setUp() public virtual override { LiquidityApproximationTest.setUp(); } function _createPool(address[] memory tokens, string memory label) internal override returns (address) { - Gyro2CLPPoolFactory factory = new Gyro2CLPPoolFactory(IVault(address(vault)), 365 days); - - PoolRoleAccounts memory roleAccounts; - - Gyro2CLPPool newPool = Gyro2CLPPool( - factory.create( - "Gyro 2CLP Pool", - "GRP", - vault.buildTokenConfig(tokens.asIERC20()), - _sqrtAlpha, - _sqrtBeta, - roleAccounts, - 0, - address(0), - ZERO_BYTES32 - ) - ); - vm.label(address(newPool), label); - return address(newPool); + IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); + return createGyro2ClpPool(tokens, rateProviders, label, vault, lp); } } diff --git a/pkg/pool-gyro/test/foundry/utils/Gyro2ClpPoolDeployer.sol b/pkg/pool-gyro/test/foundry/utils/Gyro2ClpPoolDeployer.sol new file mode 100644 index 000000000..84cca9b97 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/utils/Gyro2ClpPoolDeployer.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { PoolRoleAccounts } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import { IVaultMock } from "@balancer-labs/v3-interfaces/contracts/test/IVaultMock.sol"; + +import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; + +import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; + +import { Gyro2CLPPoolFactory } from "../../../contracts/Gyro2CLPPoolFactory.sol"; +import { Gyro2CLPPool } from "../../../contracts/Gyro2CLPPool.sol"; + +contract Gyro2ClpPoolDeployer is Test { + using CastingHelpers for address[]; + + uint256 private _sqrtAlpha = 997496867163000167; // alpha (lower price rate) = 0.995 + uint256 private _sqrtBeta = 1002496882788171068; // beta (upper price rate) = 1.005 + + function createGyro2ClpPool( + address[] memory tokens, + IRateProvider[] memory rateProviders, + string memory label, + IVaultMock vault, + address poolCreator + ) internal returns (address) { + Gyro2CLPPoolFactory factory = new Gyro2CLPPoolFactory(IVault(address(vault)), 365 days); + + PoolRoleAccounts memory roleAccounts; + + Gyro2CLPPool newPool = Gyro2CLPPool( + factory.create( + "Gyro 2CLP Pool", + "GRP", + vault.buildTokenConfig(tokens.asIERC20(), rateProviders), + _sqrtAlpha, + _sqrtBeta, + roleAccounts, + 0, + address(0), + bytes32("") + ) + ); + vm.label(address(newPool), label); + + // Cannot set the pool creator directly on a standard Balancer stable pool factory. + vault.manualSetPoolCreator(address(newPool), poolCreator); + + ProtocolFeeControllerMock feeController = ProtocolFeeControllerMock(address(vault.getProtocolFeeController())); + feeController.manualSetPoolCreator(address(newPool), poolCreator); + + return address(newPool); + } +} From fc2b6345da9af120f17bb9ca907765020891567a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 15:55:35 -0300 Subject: [PATCH 19/52] Fix naming --- pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol | 4 ++-- pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol | 4 ++-- pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol | 4 ++-- pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol | 4 ++-- .../utils/{EclpPoolDeployer.sol => GyroEclpPoolDeployer.sol} | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) rename pkg/pool-gyro/test/foundry/utils/{EclpPoolDeployer.sol => GyroEclpPoolDeployer.sol} (99%) diff --git a/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol index 3e21530d2..2dc78bfd9 100644 --- a/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol @@ -10,9 +10,9 @@ import { ERC20TestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/ import { E2eBatchSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eBatchSwap.t.sol"; -import { EclpPoolDeployer } from "./utils/EclpPoolDeployer.sol"; +import { GyroEclpPoolDeployer } from "./utils/GyroEclpPoolDeployer.sol"; -contract E2eBatchSwapECLPTest is E2eBatchSwapTest, EclpPoolDeployer { +contract E2eBatchSwapECLPTest is E2eBatchSwapTest, GyroEclpPoolDeployer { /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eBatchSwapTest tests. function _createPool(address[] memory tokens, string memory label) internal override returns (address) { IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); diff --git a/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol index 4ec77c290..8d3cc616e 100644 --- a/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol @@ -10,9 +10,9 @@ import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/Fixe import { E2eSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwap.t.sol"; -import { EclpPoolDeployer } from "./utils/EclpPoolDeployer.sol"; +import { GyroEclpPoolDeployer } from "./utils/GyroEclpPoolDeployer.sol"; -contract E2eSwapECLPTest is E2eSwapTest, EclpPoolDeployer { +contract E2eSwapECLPTest is E2eSwapTest, GyroEclpPoolDeployer { using FixedPoint for uint256; function setUp() public override { diff --git a/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol index a1b57bb38..c7009b4cb 100644 --- a/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol @@ -12,9 +12,9 @@ import { RateProviderMock } from "@balancer-labs/v3-vault/contracts/test/RatePro import { E2eSwapRateProviderTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwapRateProvider.t.sol"; import { VaultContractsDeployer } from "@balancer-labs/v3-vault/test/foundry/utils/VaultContractsDeployer.sol"; -import { EclpPoolDeployer } from "./utils/EclpPoolDeployer.sol"; +import { GyroEclpPoolDeployer } from "./utils/GyroEclpPoolDeployer.sol"; -contract E2eSwapRateProviderECLPTest is VaultContractsDeployer, E2eSwapRateProviderTest, EclpPoolDeployer { +contract E2eSwapRateProviderECLPTest is VaultContractsDeployer, E2eSwapRateProviderTest, GyroEclpPoolDeployer { using FixedPoint for uint256; /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eSwapTest tests. diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol index 17b06fd5b..58a78ac0f 100644 --- a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol @@ -8,9 +8,9 @@ import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-u import { LiquidityApproximationTest } from "@balancer-labs/v3-vault/test/foundry/LiquidityApproximation.t.sol"; -import { EclpPoolDeployer } from "./utils/EclpPoolDeployer.sol"; +import { GyroEclpPoolDeployer } from "./utils/GyroEclpPoolDeployer.sol"; -contract LiquidityApproximationECLPTest is LiquidityApproximationTest, EclpPoolDeployer { +contract LiquidityApproximationECLPTest is LiquidityApproximationTest, GyroEclpPoolDeployer { function setUp() public virtual override { LiquidityApproximationTest.setUp(); diff --git a/pkg/pool-gyro/test/foundry/utils/EclpPoolDeployer.sol b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol similarity index 99% rename from pkg/pool-gyro/test/foundry/utils/EclpPoolDeployer.sol rename to pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol index 82537aaaa..0ee02d590 100644 --- a/pkg/pool-gyro/test/foundry/utils/EclpPoolDeployer.sol +++ b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol @@ -18,7 +18,7 @@ import { GyroECLPPoolFactory } from "../../../contracts/GyroECLPPoolFactory.sol" import { GyroECLPPool } from "../../../contracts/GyroECLPPool.sol"; import { GyroECLPMath } from "../../../contracts/lib/GyroECLPMath.sol"; -contract EclpPoolDeployer is Test { +contract GyroEclpPoolDeployer is Test { using CastingHelpers for address[]; // Extracted from pool 0x2191df821c198600499aa1f0031b1a7514d7a7d9 on Mainnet. From fc6e4a5ad6a75c0550aeb9ce08053c3b5838a84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 16:17:22 -0300 Subject: [PATCH 20/52] Fix comments --- pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 292 +++++++++++-------- 1 file changed, 167 insertions(+), 125 deletions(-) diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index f3285a8f3..7f938f50d 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LicenseRef-Gyro-1.0 -// for information on licensing please see the README in the GitHub repository . +// for information on licensing please see the README in the GitHub repository +// . pragma solidity ^0.8.24; @@ -10,9 +11,10 @@ import "./GyroPoolMath.sol"; // solhint-disable private-vars-leading-underscore -/** @dev ECLP math library. Pretty much a direct translation of the python version (see `tests/`). - * We use *signed* values here because some of the intermediate results can be negative (e.g. coordinates of points in - * the untransformed circle, "prices" in the untransformed circle). +/** + * @notice ECLP math library. Pretty much a direct translation of the python version. + * @dev We use *signed* values here because some of the intermediate results can be negative (e.g. coordinates of + * points in the untransformed circle, "prices" in the untransformed circle). */ library GyroECLPMath { error RotationVectorWrong(); @@ -37,22 +39,22 @@ library GyroECLPMath { using SafeCast for uint256; using SafeCast for int256; - // Anti-overflow limits: Params and DerivedParams (static, only needs to be checked on pool creation) + // Anti-overflow limits: Params and DerivedParams (static, only needs to be checked on pool creation). int256 internal constant _ROTATION_VECTOR_NORM_ACCURACY = 1e3; // 1e-15 in normal precision int256 internal constant _MAX_STRETCH_FACTOR = 1e26; // 1e8 in normal precision int256 internal constant _DERIVED_TAU_NORM_ACCURACY_XP = 1e23; // 1e-15 in extra precision int256 internal constant _MAX_INV_INVARIANT_DENOMINATOR_XP = 1e43; // 1e5 in extra precision int256 internal constant _DERIVED_DSQ_NORM_ACCURACY_XP = 1e23; // 1e-15 in extra precision - // Anti-overflow limits: Dynamic values (checked before operations that use them) + // Anti-overflow limits: Dynamic values (checked before operations that use them). int256 internal constant _MAX_BALANCES = 1e34; // 1e16 in normal precision int256 internal constant _MAX_INVARIANT = 3e37; // 3e19 in normal precision // Note that all t values (not tp or tpp) could consist of uint's, as could all Params. But it's complicated to // convert all the time, so we make them all signed. We also store all intermediate values signed. An exception are - // the functions that are used by the contract b/c there the values are stored unsigned. + // the functions that are used by the contract because there the values are stored unsigned. struct Params { - // Price bounds (lower and upper). 0 < alpha < beta + // Price bounds (lower and upper). 0 < alpha < beta. int256 alpha; int256 beta; // Rotation vector: @@ -60,7 +62,7 @@ library GyroECLPMath { int256 c; // c = cos(-phi) >= 0. rounded to 18 decimals int256 s; // s = sin(-phi) >= 0. rounded to 18 decimals // Invariant: c^2 + s^2 == 1, i.e., the point (c, s) is normalized. - // due to rounding, this may not = 1. The term dSq in DerivedParams corrects for this in extra precision + // Due to rounding, this may not be 1. The term dSq in DerivedParams corrects for this in extra precision // Stretching factor: int256 lambda; // lambda >= 1 where lambda == 1 is the circle. @@ -75,8 +77,6 @@ library GyroECLPMath { int256 w; // from (A chi)_x = w / lambda + z int256 z; // from (A chi)_x = w / lambda + z int256 dSq; // error in c^2 + s^2 = dSq, used to correct errors in c, s, tau, u,v,w,z calculations - //int256 dAlpha; // normalization constant for tau(alpha) - //int256 dBeta; // normalization constant for tau(beta) } struct Vector2 { @@ -90,7 +90,7 @@ library GyroECLPMath { int256 c; } - /** @dev Enforces limits and approximate normalization of the rotation vector. */ + /// @dev Enforces limits and approximate normalization of the rotation vector. function validateParams(Params memory params) internal pure { if (0 > params.s || params.s > ONE) { revert RotationVectorWrong(); @@ -112,8 +112,10 @@ library GyroECLPMath { } } - /** @dev Enforces limits and approximate normalization of the derived values. - Does NOT check for internal consistency of 'derived' with 'params'. */ + /** + * @notice Enforces limits and approximate normalization of the derived values. + * @dev Does NOT check for internal consistency of 'derived' with 'params'. + */ function validateDerivedParamsLimits(Params memory params, DerivedParams memory derived) external pure { int256 norm2; norm2 = scalarProdXp(derived.tauAlpha, derived.tauAlpha); @@ -151,7 +153,7 @@ library GyroECLPMath { ret = t1.x.mulDownMag(t2.x) + t1.y.mulDownMag(t2.y); } - /// @dev scalar product for extra-precision values + /// @dev Scalar product for extra-precision values function scalarProdXp(Vector2 memory t1, Vector2 memory t2) internal pure returns (int256 ret) { ret = t1.x.mulXp(t2.x) + t1.y.mulXp(t2.y); } @@ -159,24 +161,29 @@ library GyroECLPMath { // "Methods" for Params. We could put these into a separate library and import them via 'using' to get method call // syntax. - /** @dev Calculate A t where A is given in Section 2.2 - * This is reversing rotation and scaling of the ellipse (mapping back to circle) */ + /** + * @notice Calculate A t where A is given in Section 2.2. + * @dev This is reversing rotation and scaling of the ellipse (mapping back to circle) . + */ function mulA(Params memory params, Vector2 memory tp) internal pure returns (Vector2 memory t) { // NB: This function is only used inside calculatePrice(). This is why we can make two simplifications: - // 1. We don't correct for precision of s, c using d.dSq because that level of precision is not important in this context. - // 2. We don't need to check for over/underflow b/c these are impossible in that context and given the (checked) assumptions on the various values. + // 1. We don't correct for precision of s, c using d.dSq because that level of precision is not important in + // this context; + // 2. We don't need to check for over/underflow because these are impossible in that context and given the + // (checked) assumptions on the various values. t.x = params.c.mulDownMagU(tp.x).divDownMagU(params.lambda) - params.s.mulDownMagU(tp.y).divDownMagU(params.lambda); t.y = params.s.mulDownMagU(tp.x) + params.c.mulDownMagU(tp.y); } - /** @dev Calculate virtual offset a given invariant r. - * See calculation in Section 2.1.2 Computing reserve offsets - * Note that, in contrast to virtual reserve offsets in CPMM, these are *subtracted* from the real - * reserves, moving the curve to the upper-right. They can be positive or negative, but not both can be negative. - * Calculates a = r*(A^{-1}tau(beta))_x rounding up in signed direction - * Notice that error in r is scaled by lambda, and so rounding direction is important */ + /** + * @notice Calculate virtual offset a given invariant r, see calculation in Section 2.1.2. + * @dev In contrast to virtual reserve offsets in CPMM, these are *subtracted* from the real reserves, moving the + * curve to the upper-right. They can be positive or negative, but not both can be negative. Calculates + * `a = r*(A^{-1}tau(beta))_x` rounding up in signed direction. That error in r is scaled by lambda, and so + * rounding direction is important. + */ function virtualOffset0( Params memory p, DerivedParams memory d, @@ -189,12 +196,14 @@ library GyroECLPMath { ? r.x.mulUpMagU(p.lambda).mulUpMagU(p.c).mulUpXpToNpU(termXp) : r.y.mulDownMagU(p.lambda).mulDownMagU(p.c).mulUpXpToNpU(termXp); - // use fact that tau(beta)_y > 0, so the required rounding direction is clear. + // Use fact that tau(beta)_y > 0, so the required rounding direction is clear. a = a + r.x.mulUpMagU(p.s).mulUpXpToNpU(d.tauBeta.y.divXpU(d.dSq)); } - /** @dev calculate virtual offset b given invariant r. - * Calculates b = r*(A^{-1}tau(alpha))_y rounding up in signed direction */ + /** + * @notice calculate virtual offset b given invariant r. + * @dev Calculates b = r*(A^{-1}tau(alpha))_y rounding up in signed direction + */ function virtualOffset1( Params memory p, DerivedParams memory d, @@ -207,13 +216,15 @@ library GyroECLPMath { ? r.x.mulUpMagU(p.lambda).mulUpMagU(p.s).mulUpXpToNpU(-termXp) : (-r.y).mulDownMagU(p.lambda).mulDownMagU(p.s).mulUpXpToNpU(termXp); - // use fact that tau(alpha)_y > 0, so the required rounding direction is clear. + // Use fact that tau(alpha)_y > 0, so the required rounding direction is clear. b = b + r.x.mulUpMagU(p.c).mulUpXpToNpU(d.tauAlpha.y.divXpU(d.dSq)); } - /** Maximal value for the real reserves x when the respective other balance is 0 for given invariant - * See calculation in Section 2.1.2. Calculation is ordered here for precision, but error in r is magnified by lambda - * Rounds down in signed direction */ + /** + * @notice Maximal value for the real reserves x when the respective other balance is 0 for given invariant. + * @dev See calculation in Section 2.1.2. Calculation is ordered here for precision, but error in r is magnified + * by lambda. Rounds down in signed direction + */ function maxBalances0( Params memory p, DerivedParams memory d, @@ -221,15 +232,20 @@ library GyroECLPMath { ) internal pure returns (int256 xp) { // x^+ = r lambda c (tau(beta)_x - tau(alpha)_x) + rs (tau(beta)_y - tau(alpha)_y) // account for 1 factors of dSq (2 s,c factors) - int256 termXp1 = (d.tauBeta.x - d.tauAlpha.x).divXpU(d.dSq); // note tauBeta.x > tauAlpha.x, so this is > 0 and rounding direction is clear - int256 termXp2 = (d.tauBeta.y - d.tauAlpha.y).divXpU(d.dSq); // note this may be negative, but since tauBeta.y, tauAlpha.y >= 0, it is always in [-1, 1]. + + // Note tauBeta.x > tauAlpha.x, so this is > 0 and rounding direction is clear. + int256 termXp1 = (d.tauBeta.x - d.tauAlpha.x).divXpU(d.dSq); + // Note this may be negative, but since tauBeta.y, tauAlpha.y >= 0, it is always in [-1, 1]. + int256 termXp2 = (d.tauBeta.y - d.tauAlpha.y).divXpU(d.dSq); xp = r.y.mulDownMagU(p.lambda).mulDownMagU(p.c).mulDownXpToNpU(termXp1); xp = xp + (termXp2 > 0 ? r.y.mulDownMagU(p.s) : r.x.mulUpMagU(p.s)).mulDownXpToNpU(termXp2); } - /** Maximal value for the real reserves y when the respective other balance is 0 for given invariant - * See calculation in Section 2.1.2. Calculation is ordered here for precision, but erorr in r is magnified by lambda - * Rounds down in signed direction */ + /** + * @notice Maximal value for the real reserves y when the respective other balance is 0 for given invariant. + * @dev See calculation in Section 2.1.2. Calculation is ordered here for precision, but erorr in r is magnified + * by lambda. Rounds down in signed direction + */ function maxBalances1( Params memory p, DerivedParams memory d, @@ -243,12 +259,13 @@ library GyroECLPMath { yp = yp + (termXp2 > 0 ? r.y.mulDownMagU(p.c) : r.x.mulUpMagU(p.c)).mulDownXpToNpU(termXp2); } - /** @dev Compute the invariant 'r' corresponding to the given values. The invariant can't be negative, but - * we use a signed value to store it because all the other calculations are happening with signed ints, too. - * Computes r according to Prop 13 in 2.2.1 Initialization from Real Reserves - * orders operations to achieve best precision - * Returns an underestimate and a bound on error size. - * Enforces anti-overflow limits on balances and the computed invariant in the process. */ + /** + * @notice Compute the invariant 'r' corresponding to the given values. + * @dev The invariant can't be negative, but we use a signed value to store it because all the other calculations + * are happening with signed ints, too. Computes r according to Prop 13 in 2.2.1 Initialization from Real Reserves. + * Orders operations to achieve best precision. Returns an underestimate and a bound on error size. Enforces + * anti-overflow limits on balances and the computed invariant in the process. + */ function calculateInvariantWithError( uint256[] memory balances, Params memory params, @@ -262,37 +279,39 @@ library GyroECLPMath { int256 AtAChi = calcAtAChi(x, y, params, derived); (int256 sqrt, int256 err) = calcInvariantSqrt(x, y, params, derived); - // calculate the error in the square root term, separates cases based on sqrt >= 1/2 + // Calculate the error in the square root term, separates cases based on sqrt >= 1/2 // somedayTODO: can this be improved for cases of large balances (when xp error magnifies to np) // Note: the minimum non-zero value of sqrt is 1e-9 since the minimum argument is 1e-18 if (sqrt > 0) { // err + 1 to account for O(eps_np) term ignored before err = (err + 1).divUpMagU(2 * sqrt); } else { - // in the false case here, the extra precision error does not magnify, and so the error inside the sqrt is O(1e-18) + // In the false case here, the extra precision error does not magnify, and so the error inside the sqrt is + // O(1e-18) // somedayTODO: The true case will almost surely never happen (can it be removed) err = err > 0 ? GyroPoolMath.sqrt(err.toUint256(), 5).toInt256() : int256(1e9); } - // calculate the error in the numerator, scale the error by 20 to be sure all possible terms accounted for + // Calculate the error in the numerator, scale the error by 20 to be sure all possible terms accounted for err = ((params.lambda.mulUpMagU(x + y) / ONE_XP) + err + 1) * 20; - // A chi \cdot A chi > 1, so round it up to round denominator up - // denominator uses extra precision, so we do * 1/denominator so we are sure the calculation doesn't overflow + // A chi \cdot A chi > 1, so round it up to round denominator up. + // Denominator uses extra precision, so we do * 1/denominator so we are sure the calculation doesn't overflow. int256 mulDenominator = ONE_XP.divXpU(calcAChiAChiInXp(params, derived) - ONE_XP); // NOTE: Anti-overflow limits on mulDenominator are checked on contract creation. - // as alternative, could do, but could overflow: invariant = (AtAChi.add(sqrt) - err).divXp(denominator); + // As alternative, could do, but could overflow: invariant = (AtAChi.add(sqrt) - err).divXp(denominator); int256 invariant = (AtAChi + sqrt - err).mulDownXpToNpU(mulDenominator); - // error scales if denominator is small + // Error scales if denominator is small. // NB: This error calculation computes the error in the expression "numerator / denominator", but in this code - // we actually use the formula "numerator * (1 / denominator)" to compute the invariant. This affects this line + // We actually use the formula "numerator * (1 / denominator)" to compute the invariant. This affects this line // and the one below. err = err.mulUpXpToNpU(mulDenominator); - // account for relative error due to error in the denominator - // error in denominator is O(epsilon) if lambda<1e11, scale up by 10 to be sure we catch it, and add O(eps) - // error in denominator is lambda^2 * 2e-37 and scales relative to the result / denominator + // Account for relative error due to error in the denominator. + // Error in denominator is O(epsilon) if lambda<1e11, scale up by 10 to be sure we catch it, and add O(eps). + // Error in denominator is lambda^2 * 2e-37 and scales relative to the result / denominator. // Scale by a constant to account for errors in the scaling factor itself and limited compounding. - // calculating lambda^2 w/o decimals so that the calculation will never overflow, the lost precision isn't important + // Calculating lambda^2 without decimals so that the calculation will never overflow, the lost precision isn't + // important. err = err + ((invariant.mulUpXpToNpU(mulDenominator) * ((params.lambda * params.lambda) / 1e36)) * 40) / @@ -306,7 +325,7 @@ library GyroECLPMath { return (invariant, err); } - /// @dev calculate At \cdot A chi, ignores rounding direction. We will later compensate for the rounding error. + /// @dev Calculate At \cdot A chi, ignores rounding direction. We will later compensate for the rounding error. function calcAtAChi( int256 x, int256 y, @@ -330,25 +349,27 @@ library GyroECLPMath { val = val + termNp.mulDownXpToNpU(d.v.divXpU(dSq2)); } - /// @dev calculates A chi \cdot A chi in extra precision - /// Note: this can be >1 (and involves factor of lambda^2). We can compute it in extra precision w/o overflowing b/c it will be - /// at most 38 + 16 digits (38 from decimals, 2*8 from lambda^2 if lambda=1e8) - /// Since we will only divide by this later, we will not need to worry about overflow in that operation if done in the right way - /// TODO: is rounding direction ok? + /** + * @notice Calculates A chi \cdot A chi in extra precision. + * @dev This can be >1 (and involves factor of lambda^2). We can compute it in extra precision without overflowing + * because it will be at most 38 + 16 digits (38 from decimals, 2*8 from lambda^2 if lambda=1e8). Since we will + * only divide by this later, we will not need to worry about overflow in that operation if done in the right way. + */ function calcAChiAChiInXp(Params memory p, DerivedParams memory d) internal pure returns (int256 val) { - // to save gas, pre-compute dSq^3 as it will be used 4 times + // To save gas, pre-compute dSq^3 as it will be used 4 times. int256 dSq3 = d.dSq.mulXpU(d.dSq).mulXpU(d.dSq); // (A chi)_y^2 = lambda^2 u^2 + lambda 2 u v + v^2 // account for 3 factors of dSq (6 s,c factors) // SOMEDAY: In these calcs, a calculated value is multiplied by lambda and lambda^2, resp, which implies some - // error amplification. It's fine b/c we're doing it in extra precision here, but would still be nice if it + // error amplification. It's fine because we're doing it in extra precision here, but would still be nice if it // could be avoided, perhaps by splitting up the numbers into a high and low part. val = p.lambda.mulUpMagU((2 * d.u).mulXpU(d.v).divXpU(dSq3)); - // for lambda^2 u^2 factor in rounding error in u since lambda could be big - // Note: lambda^2 is multiplied at the end to be sure the calculation doesn't overflow, but this can lose some precision + // For lambda^2 u^2 factor in rounding error in u since lambda could be big. + // Note: lambda^2 is multiplied at the end to be sure the calculation doesn't overflow, but this can lose some + // precision val = val + ((d.u + 1).mulXpU(d.u + 1).divXpU(dSq3)).mulUpMagU(p.lambda).mulUpMagU(p.lambda); - // the next line converts from extre precision to normal precision post-computation while rounding up + // The next line converts from extre precision to normal precision post-computation while rounding up. val = val + (d.v).mulXpU(d.v).divXpU(dSq3); // (A chi)_x^2 = (w/lambda + z)^2 @@ -357,7 +378,7 @@ library GyroECLPMath { val = val + termXp.mulXpU(termXp).divXpU(dSq3); } - /// @dev calculate -(At)_x ^2 (A chi)_y ^2 + (At)_x ^2, rounding down in signed direction + /// @dev Calculate -(At)_x ^2 (A chi)_y ^2 + (At)_x ^2, rounding down in signed direction function calcMinAtxAChiySqPlusAtxSq( int256 x, int256 y, @@ -380,8 +401,8 @@ library GyroECLPMath { termXp = termXp.divXpU(d.dSq.mulXpU(d.dSq).mulXpU(d.dSq).mulXpU(d.dSq)); val = (-termNp).mulDownXpToNpU(termXp); - // now calculate (At)_x^2 accounting for possible rounding error to round down - // need to do 1/dSq in a way so that there is no overflow for large balances + // Now calculate (At)_x^2 accounting for possible rounding error to round down. + // Need to do 1/dSq in a way so that there is no overflow for large balances. val = val + (termNp - 9).divDownMagU(p.lambda).divDownMagU(p.lambda).mulDownXpToNpU( @@ -389,8 +410,10 @@ library GyroECLPMath { ); } - /// @dev calculate 2(At)_x * (At)_y * (A chi)_x * (A chi)_y, ignores rounding direction - // Note: this ignores rounding direction and is corrected for later + /** + * @notice Calculate 2(At)_x * (At)_y * (A chi)_x * (A chi)_y, ignores rounding direction. + * @dev This ignores rounding direction and is corrected for later. + */ function calc2AtxAtyAChixAChiy( int256 x, int256 y, @@ -412,7 +435,7 @@ library GyroECLPMath { val = termNp.mulDownXpToNpU(termXp); } - /// @dev calculate -(At)_y ^2 (A chi)_x ^2 + (At)_y ^2, rounding down in signed direction + /// @dev Calculate -(At)_y ^2 (A chi)_x ^2 + (At)_y ^2, rounding down in signed direction. function calcMinAtyAChixSqPlusAtySq( int256 x, int256 y, @@ -433,13 +456,16 @@ library GyroECLPMath { termXp = termXp.divXpU(d.dSq.mulXpU(d.dSq).mulXpU(d.dSq).mulXpU(d.dSq)); val = (-termNp).mulDownXpToNpU(termXp); - // now calculate (At)_y^2 accounting for possible rounding error to round down - // need to do 1/dSq in a way so that there is no overflow for large balances + // Now calculate (At)_y^2 accounting for possible rounding error to round down. + // Need to do 1/dSq in a way so that there is no overflow for large balances. val = val + (termNp - 9).mulDownXpToNpU(SignedFixedPoint.ONE_XP.divXpU(d.dSq)); } - /// @dev Rounds down. Also returns an estimate for the error of the term under the sqrt (!) and without the regular - /// normal-precision error of O(1e-18). + /** + * @notice Calculates the square root of the invariant. + * @dev Rounds down. Also returns an estimate for the error of the term under the sqrt (!) and without the regular + * normal-precision error of O(1e-18). + */ function calcInvariantSqrt( int256 x, int256 y, @@ -448,45 +474,51 @@ library GyroECLPMath { ) internal pure returns (int256 val, int256 err) { val = calcMinAtxAChiySqPlusAtxSq(x, y, p, d) + calc2AtxAtyAChixAChiy(x, y, p, d); val = val + calcMinAtyAChixSqPlusAtySq(x, y, p, d); - // error inside the square root is O((x^2 + y^2) * eps_xp) + O(eps_np), where eps_xp=1e-38, eps_np=1e-18 - // note that in terms of rounding down, error corrects for calc2AtxAtyAChixAChiy() - // however, we also use this error to correct the invariant for an overestimate in swaps, it is all the same order though - // Note the O(eps_np) term will be dealt with later, so not included yet - // Note that the extra precision term doesn't propagate unless balances are > 100b + // Error inside the square root is O((x^2 + y^2) * eps_xp) + O(eps_np), where eps_xp=1e-38, eps_np=1e-18. + // Note that in terms of rounding down, error corrects for calc2AtxAtyAChixAChiy(). + // However, we also use this error to correct the invariant for an overestimate in swaps, it is all the same + // order though. + // Note the O(eps_np) term will be dealt with later, so not included yet. + // Note that the extra precision term doesn't propagate unless balances are > 100b. err = (x.mulUpMagU(x) + y.mulUpMagU(y)) / 1e38; - // we will account for the error later after the square root - // mathematically, terms in square root > 0, so treat as 0 if it is < 0 b/c of rounding error + // We will account for the error later after the square root. + // Mathematically, terms in square root > 0, so treat as 0 if it is < 0 because of rounding error. val = val > 0 ? GyroPoolMath.sqrt(val.toUint256(), 5).toInt256() : int256(0); } - /** @dev Spot price of token 0 in units of token 1. - * See Prop. 12 in 2.1.6 Computing Prices */ + /** + * @notice Spot price of token 0 in units of token 1. + * @dev See Prop. 12 in 2.1.6 Computing Prices + */ function calcSpotPrice0in1( uint256[] memory balances, Params memory params, DerivedParams memory derived, int256 invariant ) external pure returns (uint256 px) { - // shift by virtual offsets to get v(t) + // Shift by virtual offsets to get v(t). Vector2 memory r = Vector2(invariant, invariant); // ignore r rounding for spot price, precision will be lost in TWAP anyway Vector2 memory ab = Vector2(virtualOffset0(params, derived, r), virtualOffset1(params, derived, r)); Vector2 memory vec = Vector2(balances[0].toInt256() - ab.x, balances[1].toInt256() - ab.y); - // transform to circle to get Av(t) + // Transform to circle to get Av(t). vec = mulA(params, vec); - // compute prices on circle + // Compute prices on circle. Vector2 memory pc = Vector2(vec.x.divDownMagU(vec.y), ONE); // Convert prices back to ellipse // NB: These operations check for overflow because the price pc[0] might be large when vex.y is small. - // SOMEDAY I think this probably can't actually happen due to our bounds on the different values. In this case we could do this unchecked as well. + // SOMEDAY I think this probably can't actually happen due to our bounds on the different values. In this case + // we could do this unchecked as well. int256 pgx = scalarProd(pc, mulA(params, Vector2(ONE, 0))); px = pgx.divDownMag(scalarProd(pc, mulA(params, Vector2(0, ONE)))).toUint256(); } - /** @dev Check that post-swap balances obey maximal asset bounds - * newBalance = post-swap balance of one asset - * assetIndex gives the index of the provided asset (0 = X, 1 = Y) */ + /** + * @notice Check that post-swap balances obey maximal asset bounds. + * @dev newBalance = post-swap balance of one asset. assetIndex gives the index of the provided asset + * (0 = X, 1 = Y) + */ function checkAssetBounds( Params memory params, DerivedParams memory derived, @@ -558,14 +590,17 @@ library GyroECLPMath { if (!(amountOut <= balances[ixOut])) revert AssetBoundsExceeded(); int256 balOutNew = (balances[ixOut] - amountOut).toInt256(); int256 balInNew = calcGiven(balOutNew, params, derived, invariant); - // The checks in the following two lines should really always succeed; we keep them as extra safety against numerical error. + // The checks in the following two lines should really always succeed; we keep them as extra safety against + // numerical error. checkAssetBounds(params, derived, invariant, balInNew, ixIn); amountIn = balInNew.toUint256() - balances[ixIn]; } - /** @dev Variables are named for calculating y given x - * to calculate x given y, change x->y, s->c, c->s, a_>b, b->a, tauBeta.x -> -tauAlpha.x, tauBeta.y -> tauAlpha.y - * calculates an overestimate of calculated reserve post-swap */ + /** + * @dev Variables are named for calculating y given x. To calculate x given y, change x->y, s->c, c->s, a_>b, b->a, + * tauBeta.x -> -tauAlpha.x, tauBeta.y -> tauAlpha.y. Also, calculates an overestimate of calculated reserve + * post-swap. + */ function solveQuadraticSwap( int256 lambda, int256 x, @@ -576,17 +611,17 @@ library GyroECLPMath { Vector2 memory tauBeta, int256 dSq ) internal pure returns (int256) { - // x component will round up, y will round down, use extra precision + // x component will round up, y will round down, use extra precision. Vector2 memory lamBar; lamBar.x = SignedFixedPoint.ONE_XP - SignedFixedPoint.ONE_XP.divDownMagU(lambda).divDownMagU(lambda); - // Note: The following cannot become negative even with errors because we require lambda >= 1 and - // divUpMag returns the exact result if the quotient is representable in 18 decimals. + // Note: The following cannot become negative even with errors because we require lambda >= 1 and divUpMag + // returns the exact result if the quotient is representable in 18 decimals. lamBar.y = SignedFixedPoint.ONE_XP - SignedFixedPoint.ONE_XP.divUpMagU(lambda).divUpMagU(lambda); - // using qparams struct to avoid "stack too deep" + // Using qparams struct to avoid "stack too deep". QParams memory q; - // shift by the virtual offsets - // note that we want an overestimate of offset here so that -x'*lambar*s*c is overestimated in signed direction - // account for 1 factor of dSq (2 s,c factors) + // Shift by the virtual offsets. + // Note that we want an overestimate of offset here so that -x'*lambar*s*c is overestimated in signed + // direction. Account for 1 factor of dSq (2 s,c factors). int256 xp = x - ab.x; if (xp > 0) { q.b = (-xp).mulDownMagU(s).mulDownMagU(c).mulUpXpToNpU(lamBar.y.divXpU(dSq)); @@ -594,37 +629,41 @@ library GyroECLPMath { q.b = (-xp).mulUpMagU(s).mulUpMagU(c).mulUpXpToNpU(lamBar.x.divXpU(dSq) + 1); } - // x component will round up, y will round down, use extra precision - // account for 1 factor of dSq (2 s,c factors) + // x component will round up, y will round down, use extra precision. + // Account for 1 factor of dSq (2 s,c factors). Vector2 memory sTerm; - // we wil take sTerm = 1 - sTerm below, using multiple lines to avoid "stack too deep" + // We wil take sTerm = 1 - sTerm below, using multiple lines to avoid "stack too deep". sTerm.x = lamBar.y.mulDownMagU(s).mulDownMagU(s).divXpU(dSq); sTerm.y = lamBar.x.mulUpMagU(s); sTerm.y = sTerm.y.mulUpMagU(s).divXpU(dSq + 1) + 1; // account for rounding error in dSq, divXp sTerm = Vector2(SignedFixedPoint.ONE_XP - sTerm.x, SignedFixedPoint.ONE_XP - sTerm.y); - // ^^ NB: The components of sTerm are non-negative: We only need to worry about sTerm.y. This is non-negative b/c, because of bounds on lambda lamBar <= 1 - 1e-16, and division by dSq ensures we have enough precision so that rounding errors are never magnitude 1e-16. + // ^^ NB: The components of sTerm are non-negative: We only need to worry about sTerm.y. This is non-negative + // because, because of bounds on lambda lamBar <= 1 - 1e-16, and division by dSq ensures we have enough + // precision so that rounding errors are never magnitude 1e-16. - // now compute the argument of the square root + // Now compute the argument of the square root. q.c = -calcXpXpDivLambdaLambda(x, r, lambda, s, c, tauBeta, dSq); q.c = q.c + r.y.mulDownMagU(r.y).mulDownXpToNpU(sTerm.y); - // the square root is always being subtracted, so round it down to overestimate the end balance - // mathematically, terms in square root > 0, so treat as 0 if it is < 0 b/c of rounding error + // The square root is always being subtracted, so round it down to overestimate the end balance. + // Mathematically, terms in square root > 0, so treat as 0 if it is < 0 because of rounding error. q.c = q.c > 0 ? GyroPoolMath.sqrt(q.c.toUint256(), 5).toInt256() : int256(0); - // calculate the result in q.a + // Calculate the result in q.a. if (q.b - q.c > 0) { q.a = (q.b - q.c).mulUpXpToNpU(SignedFixedPoint.ONE_XP.divXpU(sTerm.y) + 1); } else { q.a = (q.b - q.c).mulUpXpToNpU(SignedFixedPoint.ONE_XP.divXpU(sTerm.x)); } - // lastly, add the offset, note that we want an overestimate of offset here + // Lastly, add the offset, note that we want an overestimate of offset here. return q.a + ab.y; } - /** @dev Calculates x'x'/λ^2 where x' = x - b = x - r (A^{-1}tau(beta))_x - * calculates an overestimate - * to calculate y'y', change x->y, s->c, c->s, tauBeta.x -> -tauAlpha.x, tauBeta.y -> tauAlpha.y */ + /** + * @notice Calculates x'x'/λ^2 where x' = x - b = x - r (A^{-1}tau(beta))_x + * @dev Calculates an overestimate. To calculate y'y', change x->y, s->c, c->s, tauBeta.x -> -tauAlpha.x, + * tauBeta.y -> tauAlpha.y + */ function calcXpXpDivLambdaLambda( int256 x, Vector2 memory r, // overestimate in x component, underestimate in y @@ -663,7 +702,7 @@ library GyroECLPMath { } else { q.b = (-r.y).mulDownMagU(x).mulDownMagU(2 * c).mulUpXpToNpU(tauBeta.x.divXpU(dSq)); } - // q.a later needs to be divided by lambda + // q.a later needs to be divided by lambda. q.a = q.a + q.b; // q.b = r^2 s^2 tau(beta)_y^2 @@ -680,7 +719,7 @@ library GyroECLPMath { q.b = q.b + q.c + x.mulUpMagU(x); q.b = q.b > 0 ? q.b.divUpMagU(lambda) : q.b.divDownMagU(lambda); - // remaining calculation is (q.a + q.b) / lambda + // Remaining calculation is (q.a + q.b) / lambda q.a = q.a + q.b; q.a = q.a > 0 ? q.a.divUpMagU(lambda) : q.a.divDownMagU(lambda); @@ -691,17 +730,19 @@ library GyroECLPMath { return (val.mulUpXpToNpU(termXp)) + q.a; } - /** @dev compute y such that (x, y) satisfy the invariant at the given parameters. - * Note that we calculate an overestimate of y - * See Prop 14 in section 2.2.2 Trade Execution */ + /** + * @notice compute y such that (x, y) satisfy the invariant at the given parameters. + * @dev We calculate an overestimate of y. See Prop 14 in section 2.2.2 Trade Execution + */ function calcYGivenX( int256 x, Params memory params, DerivedParams memory d, Vector2 memory r // overestimate in x component, underestimate in y ) internal pure returns (int256 y) { - // want to overestimate the virtual offsets except in a particular setting that will be corrected for later - // note that the error correction in the invariant should more than make up for uncaught rounding directions (in 38 decimals) in virtual offsets + // Want to overestimate the virtual offsets except in a particular setting that will be corrected for later. + // Note that the error correction in the invariant should more than make up for uncaught rounding directions + // (in 38 decimals) in virtual offsets. Vector2 memory ab = Vector2(virtualOffset0(params, d, r), virtualOffset1(params, d, r)); y = solveQuadraticSwap(params.lambda, x, params.s, params.c, r, ab, d.tauBeta, d.dSq); } @@ -712,10 +753,11 @@ library GyroECLPMath { DerivedParams memory d, Vector2 memory r // overestimate in x component, underestimate in y ) internal pure returns (int256 x) { - // want to overestimate the virtual offsets except in a particular setting that will be corrected for later - // note that the error correction in the invariant should more than make up for uncaught rounding directions (in 38 decimals) in virtual offsets + // Want to overestimate the virtual offsets except in a particular setting that will be corrected for later. + // Note that the error correction in the invariant should more than make up for uncaught rounding directions + // (in 38 decimals) in virtual offsets. Vector2 memory ba = Vector2(virtualOffset1(params, d, r), virtualOffset0(params, d, r)); - // change x->y, s->c, c->s, b->a, a->b, tauBeta.x -> -tauAlpha.x, tauBeta.y -> tauAlpha.y vs calcYGivenX + // Change x->y, s->c, c->s, b->a, a->b, tauBeta.x -> -tauAlpha.x, tauBeta.y -> tauAlpha.y vs calcYGivenX. x = solveQuadraticSwap( params.lambda, y, From b5330d5ea6f2e703bfc1cb5b9da81b6ca25c6ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 16:22:32 -0300 Subject: [PATCH 21/52] Lint --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 12 ++++++------ pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 1983a59b2..741284aa1 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -91,7 +91,7 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { ( GyroECLPMath.Params memory eclpParams, GyroECLPMath.DerivedParams memory derivedECLPParams - ) = reconstructECLPParams(); + ) = _reconstructECLPParams(); (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( balancesLiveScaled18, @@ -115,7 +115,7 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { ( GyroECLPMath.Params memory eclpParams, GyroECLPMath.DerivedParams memory derivedECLPParams - ) = reconstructECLPParams(); + ) = _reconstructECLPParams(); GyroECLPMath.Vector2 memory invariant; { @@ -152,7 +152,7 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { ( GyroECLPMath.Params memory eclpParams, GyroECLPMath.DerivedParams memory derivedECLPParams - ) = reconstructECLPParams(); + ) = _reconstructECLPParams(); GyroECLPMath.Vector2 memory invariant; { (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( @@ -192,8 +192,8 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { } /** @dev reconstructs ECLP params structs from immutable arrays */ - function reconstructECLPParams() - internal + function _reconstructECLPParams() + private view returns (GyroECLPMath.Params memory params, GyroECLPMath.DerivedParams memory d) { @@ -213,7 +213,7 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { view returns (GyroECLPMath.Params memory params, GyroECLPMath.DerivedParams memory d) { - return reconstructECLPParams(); + return _reconstructECLPParams(); } /// @inheritdoc ISwapFeePercentageBounds diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index 7f938f50d..ffcf45ac6 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -277,7 +277,7 @@ library GyroECLPMath { revert MaxAssetsExceeded(); } - int256 AtAChi = calcAtAChi(x, y, params, derived); + int256 atAChi = calcAtAChi(x, y, params, derived); (int256 sqrt, int256 err) = calcInvariantSqrt(x, y, params, derived); // Calculate the error in the square root term, separates cases based on sqrt >= 1/2 // somedayTODO: can this be improved for cases of large balances (when xp error magnifies to np) @@ -300,7 +300,7 @@ library GyroECLPMath { // NOTE: Anti-overflow limits on mulDenominator are checked on contract creation. // As alternative, could do, but could overflow: invariant = (AtAChi.add(sqrt) - err).divXp(denominator); - int256 invariant = (AtAChi + sqrt - err).mulDownXpToNpU(mulDenominator); + int256 invariant = (atAChi + sqrt - err).mulDownXpToNpU(mulDenominator); // Error scales if denominator is small. // NB: This error calculation computes the error in the expression "numerator / denominator", but in this code // We actually use the formula "numerator * (1 / denominator)" to compute the invariant. This affects this line @@ -497,7 +497,8 @@ library GyroECLPMath { int256 invariant ) external pure returns (uint256 px) { // Shift by virtual offsets to get v(t). - Vector2 memory r = Vector2(invariant, invariant); // ignore r rounding for spot price, precision will be lost in TWAP anyway + // Ignore r rounding for spot price, precision will be lost in TWAP anyway. + Vector2 memory r = Vector2(invariant, invariant); Vector2 memory ab = Vector2(virtualOffset0(params, derived, r), virtualOffset1(params, derived, r)); Vector2 memory vec = Vector2(balances[0].toInt256() - ab.x, balances[1].toInt256() - ab.y); From 7288a9fe76572c7750d0a56cf976c51208840a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 21:55:36 -0300 Subject: [PATCH 22/52] Implement CI of Gyro --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ff6db7e1..3c348b41b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,6 +143,20 @@ jobs: - name: Test run: yarn workspace @balancer-labs/v3-pool-stable test:forge + test-forge-pool-gyro: + runs-on: ubuntu-latest + needs: lint-and-build + steps: + - uses: actions/checkout@v3 + - name: Set up environment + uses: ./.github/actions/setup + - uses: actions/download-artifact@v4 + with: + name: built-contracts + path: pkg/ + - name: Test + run: yarn workspace @balancer-labs/v3-pool-gyro test:forge + test-hardhat-pool-utils: runs-on: ubuntu-latest needs: lint-and-build From 7bffe4e22806c06de22d4d24b3cb0269fd428ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno=20-=20Balancer=20Labs?= Date: Wed, 16 Oct 2024 21:55:41 -0300 Subject: [PATCH 23/52] Update pkg/pool-gyro/contracts/Gyro2CLPPool.sol Co-authored-by: Juan Ignacio Ubeira --- pkg/pool-gyro/contracts/Gyro2CLPPool.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/pool-gyro/contracts/Gyro2CLPPool.sol b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol index 874d271e6..e7c6a2350 100644 --- a/pkg/pool-gyro/contracts/Gyro2CLPPool.sol +++ b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol @@ -94,9 +94,9 @@ contract Gyro2CLPPool is IBasePool, BalancerPoolToken { invariant = invariant.mulUp(invariantRatio); uint256 squareNewInv = invariant * invariant; // L / sqrt(beta) - uint256 a = invariant.divUp(sqrtParams[1]); + uint256 a = invariant.divDown(sqrtParams[1]); // L * sqrt(alpha) - uint256 b = invariant.mulUp(sqrtParams[0]); + uint256 b = invariant.mulDown(sqrtParams[0]); if (tokenInIndex == 0) { // if newBalance = newX From 937dca7909689145ee0f6344363666046c3b8181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 21:58:14 -0300 Subject: [PATCH 24/52] Fix comment --- pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol index c0d147db8..fb890f771 100644 --- a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol @@ -56,6 +56,10 @@ library Gyro2CLPMath { * @dev It works with a special case of quadratic that works nicely without negative numbers and assumes a > 0, * b < 0, and c <= 0. * + * @param balances Pool balances + * @param sqrtAlpha Square root of Gyro's 2CLP alpha parameter + * @param sqrtBeta Square root of Gyro's 2CLP beta parameter + * @param rounding Rounding direction of the invariant, which will be calculated using the quadratic terms * @return a Bhaskara's `a` term * @return mb Bhaskara's `b` term, negative (stands for minus b) * @return bSquare Bhaskara's `b^2` term. The calculation is optimized to be more precise than just b*b @@ -77,7 +81,8 @@ library Gyro2CLPMath { { // `a` follows the opposite rounding than `b` and `c`, since the most significant term is in the // denominator of Bhaskara's formula. To round invariant up, we need to round `a` down, which means that - // the division `sqrtAlpha/sqrtBeta` needs to be rounded up. + // the division `sqrtAlpha/sqrtBeta` needs to be rounded up. In other words, if the given rounding + // direction is UP, 'a' will be rounded DOWN and vice versa. a = FixedPoint.ONE - _divUpOrDown(sqrtAlpha, sqrtBeta); // `b` is a term in the numerator and should be rounded up if we want to increase the invariant. From 7e18709b04ad2f94f34c5917906d33c0b30a3acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 21:59:49 -0300 Subject: [PATCH 25/52] Implement _mulDownOrUp --- pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol index fb890f771..164ad4623 100644 --- a/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/Gyro2CLPMath.sol @@ -78,6 +78,11 @@ library Gyro2CLPMath { ? FixedPoint.mulDown : FixedPoint.mulUp; + // This is the inverse of mulUpAndDown, used to round denominator terms. + function(uint256, uint256) pure returns (uint256) _mulDownOrUp = rounding == Rounding.ROUND_DOWN + ? FixedPoint.mulUp + : FixedPoint.mulDown; + { // `a` follows the opposite rounding than `b` and `c`, since the most significant term is in the // denominator of Bhaskara's formula. To round invariant up, we need to round `a` down, which means that @@ -96,7 +101,7 @@ library Gyro2CLPMath { // `b^2 = x^2 * alpha + x*y*2*sqrt(alpha/beta) + y^2 / beta` bSquare = _mulUpOrDown(_mulUpOrDown(balances[0], balances[0]), _mulUpOrDown(sqrtAlpha, sqrtAlpha)); uint256 bSq2 = _divUpOrDown(2 * _mulUpOrDown(_mulUpOrDown(balances[0], balances[1]), sqrtAlpha), sqrtBeta); - uint256 bSq3 = _divUpOrDown(_mulUpOrDown(balances[1], balances[1]), sqrtBeta.mulUp(sqrtBeta)); + uint256 bSq3 = _divUpOrDown(_mulUpOrDown(balances[1], balances[1]), _mulDownOrUp(sqrtBeta, sqrtBeta)); bSquare = bSquare + bSq2 + bSq3; } From 47fa22da82de2919486aa2e84b2adedb503ba79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 22:03:35 -0300 Subject: [PATCH 26/52] Restore the balance to previous balances --- pkg/pool-gyro/test/foundry/ComputeBalance2CLP.t.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/pool-gyro/test/foundry/ComputeBalance2CLP.t.sol b/pkg/pool-gyro/test/foundry/ComputeBalance2CLP.t.sol index f091468e2..293e0f072 100644 --- a/pkg/pool-gyro/test/foundry/ComputeBalance2CLP.t.sol +++ b/pkg/pool-gyro/test/foundry/ComputeBalance2CLP.t.sol @@ -45,13 +45,15 @@ contract ComputeBalance2CLPTest is BaseVaultTest { deltaX = bound(deltaX, 1e16, 1e30); balances[0] = balances[0] + deltaX; uint256 newInvariant = _gyroPool.computeInvariant(balances, Rounding.ROUND_DOWN); - balances[0] = balances[0] - deltaX; + + // Restores the balances to original balances, to calculate computeBalance properly. + balances[0] = balanceX; uint256 invariantRatio = newInvariant.divDown(oldInvariant); uint256 newXBalance = _gyroPool.computeBalance(balances, 0, invariantRatio); // 0.000000000002% error - assertApproxEqRel(newXBalance, balanceX + deltaX, 2e4); + assertApproxEqRel(newXBalance, balanceX + deltaX, 2e4, "Balance of X does not match"); } function testComputeNewYBalance__Fuzz(uint256 balanceX, uint256 balanceY, uint256 deltaY) public view { @@ -70,12 +72,14 @@ contract ComputeBalance2CLPTest is BaseVaultTest { deltaY = bound(deltaY, 1e16, 1e30); balances[1] = balances[1] + deltaY; uint256 newInvariant = _gyroPool.computeInvariant(balances, Rounding.ROUND_DOWN); - balances[1] = balances[1] - deltaY; + + // Restores the balances to original balances, to calculate computeBalance properly. + balances[1] = balanceY; uint256 invariantRatio = newInvariant.divDown(oldInvariant); uint256 newYBalance = _gyroPool.computeBalance(balances, 1, invariantRatio); // 0.000000000002% error - assertApproxEqRel(newYBalance, balanceY + deltaY, 2e4); + assertApproxEqRel(newYBalance, balanceY + deltaY, 2e4, "Balance of Y does not match"); } } From e57fdfe6d253cee6e0dbcde79faf2052d48cfa10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 16 Oct 2024 22:25:49 -0300 Subject: [PATCH 27/52] Trying to use Hardhat artifacts --- pkg/pool-gyro/hardhat.config.ts | 16 ++++++---- pkg/pool-gyro/package.json | 2 +- .../foundry/utils/Gyro2ClpPoolDeployer.sol | 29 +++++++++++++++++-- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/pkg/pool-gyro/hardhat.config.ts b/pkg/pool-gyro/hardhat.config.ts index f42e8cc79..a1f1c9946 100644 --- a/pkg/pool-gyro/hardhat.config.ts +++ b/pkg/pool-gyro/hardhat.config.ts @@ -1,13 +1,17 @@ -import '@nomicfoundation/hardhat-ethers'; +import { HardhatUserConfig } from 'hardhat/config'; + +import { hardhatBaseConfig } from '@balancer-labs/v3-common'; + import '@nomicfoundation/hardhat-toolbox'; +import '@nomicfoundation/hardhat-ethers'; import '@typechain/hardhat'; import 'hardhat-ignore-warnings'; import 'hardhat-gas-reporter'; +import 'hardhat-contract-sizer'; +import { warnings } from '@balancer-labs/v3-common/hardhat-base-config'; -import { hardhatBaseConfig } from '@balancer-labs/v3-common'; - -export default { +const config: HardhatUserConfig = { networks: { hardhat: { allowUnlimitedContractSize: true, @@ -16,5 +20,7 @@ export default { solidity: { compilers: hardhatBaseConfig.compilers, }, - warnings: hardhatBaseConfig.warnings, + warnings, }; + +export default config; diff --git a/pkg/pool-gyro/package.json b/pkg/pool-gyro/package.json index 2d314d688..76d65b904 100644 --- a/pkg/pool-gyro/package.json +++ b/pkg/pool-gyro/package.json @@ -26,7 +26,7 @@ "prettier": "npx prettier --write --plugin=prettier-plugin-solidity 'contracts/**/*.sol' 'test/**/*.sol'", "test": "yarn test:hardhat && yarn test:forge", "test:hardhat": "hardhat test", - "test:forge": "forge test --ffi -vvv", + "test:forge": "yarn build && REUSING_HARDHAT_ARTIFACTS=true forge test --ffi -vvv", "test:stress": "FOUNDRY_PROFILE=intense forge test --ffi -vvv", "coverage": "coverage.sh", "gas": "REPORT_GAS=true hardhat test", diff --git a/pkg/pool-gyro/test/foundry/utils/Gyro2ClpPoolDeployer.sol b/pkg/pool-gyro/test/foundry/utils/Gyro2ClpPoolDeployer.sol index 84cca9b97..f301bc590 100644 --- a/pkg/pool-gyro/test/foundry/utils/Gyro2ClpPoolDeployer.sol +++ b/pkg/pool-gyro/test/foundry/utils/Gyro2ClpPoolDeployer.sol @@ -9,6 +9,7 @@ import { PoolRoleAccounts } from "@balancer-labs/v3-interfaces/contracts/vault/V import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; import { IVaultMock } from "@balancer-labs/v3-interfaces/contracts/test/IVaultMock.sol"; +import { BaseContractsDeployer } from "@balancer-labs/v3-solidity-utils/test/foundry/utils/BaseContractsDeployer.sol"; import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; @@ -16,12 +17,21 @@ import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/tes import { Gyro2CLPPoolFactory } from "../../../contracts/Gyro2CLPPoolFactory.sol"; import { Gyro2CLPPool } from "../../../contracts/Gyro2CLPPool.sol"; -contract Gyro2ClpPoolDeployer is Test { +contract Gyro2ClpPoolDeployer is BaseContractsDeployer { using CastingHelpers for address[]; uint256 private _sqrtAlpha = 997496867163000167; // alpha (lower price rate) = 0.995 uint256 private _sqrtBeta = 1002496882788171068; // beta (upper price rate) = 1.005 + string private artifactsRootDir = "artifacts/"; + + constructor() { + // if this external artifact path exists, it means we are running outside of this repo + if (vm.exists("artifacts/@balancer-labs/v3-pool-gyro/")) { + artifactsRootDir = "artifacts/@balancer-labs/v3-pool-gyro/"; + } + } + function createGyro2ClpPool( address[] memory tokens, IRateProvider[] memory rateProviders, @@ -29,7 +39,7 @@ contract Gyro2ClpPoolDeployer is Test { IVaultMock vault, address poolCreator ) internal returns (address) { - Gyro2CLPPoolFactory factory = new Gyro2CLPPoolFactory(IVault(address(vault)), 365 days); + Gyro2CLPPoolFactory factory = deployGyro2CLPPoolFactory(vault); PoolRoleAccounts memory roleAccounts; @@ -56,4 +66,19 @@ contract Gyro2ClpPoolDeployer is Test { return address(newPool); } + + function deployGyro2CLPPoolFactory(IVault vault) internal returns (Gyro2CLPPoolFactory) { + if (reusingArtifacts) { + return + Gyro2CLPPoolFactory( + deployCode(_computeGyro2CLPPath(type(Gyro2CLPPoolFactory).name), abi.encode(vault, 365 days)) + ); + } else { + return new Gyro2CLPPoolFactory(vault, 365 days); + } + } + + function _computeGyro2CLPPath(string memory name) private view returns (string memory) { + return string(abi.encodePacked(artifactsRootDir, "contracts/", name, ".sol/", name, ".json")); + } } From 54868d0e852af3b9c1d64ee19236b8a287e2ce1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 17 Oct 2024 10:13:06 -0300 Subject: [PATCH 28/52] Fix hardhat imports --- pkg/pool-gyro/contracts/test/HardhatImports.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pkg/pool-gyro/contracts/test/HardhatImports.sol diff --git a/pkg/pool-gyro/contracts/test/HardhatImports.sol b/pkg/pool-gyro/contracts/test/HardhatImports.sol new file mode 100644 index 000000000..88ea06ce5 --- /dev/null +++ b/pkg/pool-gyro/contracts/test/HardhatImports.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +// This file is needed to compile artifacts from another repository using Hardhat. +import { VaultMock } from "@balancer-labs/v3-vault/contracts/test/VaultMock.sol"; +import { BasicAuthorizerMock } from "@balancer-labs/v3-vault/contracts/test/BasicAuthorizerMock.sol"; +import { VaultAdminMock } from "@balancer-labs/v3-vault/contracts/test/VaultAdminMock.sol"; +import { VaultExtensionMock } from "@balancer-labs/v3-vault/contracts/test/VaultExtensionMock.sol"; +import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; +import { RouterMock } from "@balancer-labs/v3-vault/contracts/test/RouterMock.sol"; +import { BatchRouterMock } from "@balancer-labs/v3-vault/contracts/test/BatchRouterMock.sol"; +import { PoolHooksMock } from "@balancer-labs/v3-vault/contracts/test/PoolHooksMock.sol"; +import { RateProviderMock } from "@balancer-labs/v3-vault/contracts/test/RateProviderMock.sol"; From cd1ecfbc20d4f720756254544f6558a379dd0b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 18 Oct 2024 10:02:41 -0300 Subject: [PATCH 29/52] Add Fungibility test to Gyro 2CLP --- .../test/foundry/FungibilityGyro2CLP.t.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 pkg/pool-gyro/test/foundry/FungibilityGyro2CLP.t.sol diff --git a/pkg/pool-gyro/test/foundry/FungibilityGyro2CLP.t.sol b/pkg/pool-gyro/test/foundry/FungibilityGyro2CLP.t.sol new file mode 100644 index 000000000..6734a3abe --- /dev/null +++ b/pkg/pool-gyro/test/foundry/FungibilityGyro2CLP.t.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; + +import { FungibilityTest } from "@balancer-labs/v3-vault/test/foundry/Fungibility.t.sol"; + +import { Gyro2ClpPoolDeployer } from "./utils/Gyro2ClpPoolDeployer.sol"; + +contract FungibilityGyro2CLPTest is FungibilityTest, Gyro2ClpPoolDeployer { + /// @notice Overrides BaseVaultTest _createPool(). This pool is used by FungibilityTest. + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); + return createGyro2ClpPool(tokens, rateProviders, label, vault, lp); + } +} From be846e14dd8ba03163d0b53bd0f312d24c09d974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 18 Oct 2024 10:06:09 -0300 Subject: [PATCH 30/52] Implement fungibility test to Gyro E-CLP --- .../test/foundry/E2eBatchSwapECLP.t.sol | 2 +- pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol | 2 +- .../foundry/E2eSwapRateProviderECLP.t.sol | 2 +- .../test/foundry/FungibilityGyroECLP.t.sol | 19 +++++++++++++++++++ .../foundry/LiquidityApproximationECLP.t.sol | 2 +- .../foundry/utils/GyroEclpPoolDeployer.sol | 2 +- 6 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 pkg/pool-gyro/test/foundry/FungibilityGyroECLP.t.sol diff --git a/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol index 2dc78bfd9..e738169de 100644 --- a/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol @@ -16,7 +16,7 @@ contract E2eBatchSwapECLPTest is E2eBatchSwapTest, GyroEclpPoolDeployer { /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eBatchSwapTest tests. function _createPool(address[] memory tokens, string memory label) internal override returns (address) { IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); - return createEclpPool(tokens, rateProviders, label, vault, lp); + return createGyroEclpPool(tokens, rateProviders, label, vault, lp); } function _setUpVariables() internal override { diff --git a/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol index 8d3cc616e..c768afd05 100644 --- a/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol @@ -22,7 +22,7 @@ contract E2eSwapECLPTest is E2eSwapTest, GyroEclpPoolDeployer { /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eSwapTest tests. function _createPool(address[] memory tokens, string memory label) internal override returns (address) { IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); - return createEclpPool(tokens, rateProviders, label, vault, lp); + return createGyroEclpPool(tokens, rateProviders, label, vault, lp); } function setUpVariables() internal override { diff --git a/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol index c7009b4cb..02d6feb97 100644 --- a/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol @@ -29,7 +29,7 @@ contract E2eSwapRateProviderECLPTest is VaultContractsDeployer, E2eSwapRateProvi rateProviders[tokenAIdx] = IRateProvider(address(rateProviderTokenA)); rateProviders[tokenBIdx] = IRateProvider(address(rateProviderTokenB)); - return createEclpPool(tokens, rateProviders, label, vault, lp); + return createGyroEclpPool(tokens, rateProviders, label, vault, lp); } function calculateMinAndMaxSwapAmounts() internal virtual override { diff --git a/pkg/pool-gyro/test/foundry/FungibilityGyroECLP.t.sol b/pkg/pool-gyro/test/foundry/FungibilityGyroECLP.t.sol new file mode 100644 index 000000000..5fef55882 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/FungibilityGyroECLP.t.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; + +import { FungibilityTest } from "@balancer-labs/v3-vault/test/foundry/Fungibility.t.sol"; + +import { GyroEclpPoolDeployer } from "./utils/GyroEclpPoolDeployer.sol"; + +contract FungibilityGyroECLPTest is FungibilityTest, GyroEclpPoolDeployer { + /// @notice Overrides BaseVaultTest _createPool(). This pool is used by FungibilityTest. + function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); + return createGyroEclpPool(tokens, rateProviders, label, vault, lp); + } +} diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol index 58a78ac0f..e10058e8e 100644 --- a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol @@ -20,6 +20,6 @@ contract LiquidityApproximationECLPTest is LiquidityApproximationTest, GyroEclpP function _createPool(address[] memory tokens, string memory label) internal override returns (address) { IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); - return createEclpPool(tokens, rateProviders, label, vault, lp); + return createGyroEclpPool(tokens, rateProviders, label, vault, lp); } } diff --git a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol index 0ee02d590..045ac9873 100644 --- a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol +++ b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol @@ -38,7 +38,7 @@ contract GyroEclpPoolDeployer is Test { int256 internal _z = -28859471639991253843240999485797747790; int256 internal _dSq = 99999999999999999886624093342106115200; - function createEclpPool( + function createGyroEclpPool( address[] memory tokens, IRateProvider[] memory rateProviders, string memory label, From 58053361f5421bf7662be5a8496510be910cd2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 21 Oct 2024 16:24:10 -0300 Subject: [PATCH 31/52] Fix Batch Swap test --- pkg/vault/test/foundry/E2eBatchSwap.t.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/vault/test/foundry/E2eBatchSwap.t.sol b/pkg/vault/test/foundry/E2eBatchSwap.t.sol index 9db729099..0907ec2b1 100644 --- a/pkg/vault/test/foundry/E2eBatchSwap.t.sol +++ b/pkg/vault/test/foundry/E2eBatchSwap.t.sol @@ -213,7 +213,12 @@ contract E2eBatchSwapTest is BaseVaultTest { vm.stopPrank(); // Error tolerance is proportional to swap fee percentage. - assertApproxEqRel(amountIn, exactAmountIn, poolFeePercentage, "ExactIn and ExactOut amountsIn should match"); + assertApproxEqRel( + amountIn, + exactAmountIn, + 2 * poolFeePercentage, + "ExactIn and ExactOut amountsIn should match" + ); } function testExactInRepeatEachOperation__Fuzz( From b6bba1e277459127c4033d43bbf14cc56946f7ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 30 Oct 2024 12:15:21 -0300 Subject: [PATCH 32/52] Create interface for GyroECLPPool --- .../contracts/pool-gyro/IGyro2CLPPool.sol | 1 - .../contracts/pool-gyro/IGyroECLPPool.sol | 58 +++++ pkg/pool-gyro/contracts/GyroECLPPool.sol | 65 ++--- .../contracts/GyroECLPPoolFactory.sol | 8 +- pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 244 +++++++++--------- .../foundry/utils/GyroEclpPoolDeployer.sol | 42 ++- 6 files changed, 233 insertions(+), 185 deletions(-) create mode 100644 pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol diff --git a/pkg/interfaces/contracts/pool-gyro/IGyro2CLPPool.sol b/pkg/interfaces/contracts/pool-gyro/IGyro2CLPPool.sol index f6c9e0df4..c1e972f68 100644 --- a/pkg/interfaces/contracts/pool-gyro/IGyro2CLPPool.sol +++ b/pkg/interfaces/contracts/pool-gyro/IGyro2CLPPool.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.24; -import { Rounding } from "../vault/VaultTypes.sol"; import { IBasePool } from "../vault/IBasePool.sol"; interface IGyro2CLPPool is IBasePool { diff --git a/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol b/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol new file mode 100644 index 000000000..66b0f6c3d --- /dev/null +++ b/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IBasePool } from "../vault/IBasePool.sol"; + +interface IGyroECLPPool is IBasePool { + event ECLPParamsValidated(bool paramsValidated); + event ECLPDerivedParamsValidated(bool derivedParamsValidated); + + /** + * @notice Gyro 2CLP pool configuration. + * @param name Pool name + * @param symbol Pool symbol + * @param sqrtAlpha Square root of alpha (the lowest price in the price interval of the 2CLP price curve) + * @param sqrtBeta Square root of beta (the highest price in the price interval of the 2CLP price curve) + */ + struct GyroECLPPoolParams { + string name; + string symbol; + Params eclpParams; + DerivedParams derivedEclpParams; + } + + // Note that all t values (not tp or tpp) could consist of uint's, as could all Params. But it's complicated to + // convert all the time, so we make them all signed. We also store all intermediate values signed. An exception are + // the functions that are used by the contract because there the values are stored unsigned. + struct Params { + // Price bounds (lower and upper). 0 < alpha < beta. + int256 alpha; + int256 beta; + // Rotation vector: + // phi in (-90 degrees, 0] is the implicit rotation vector. It's stored as a point: + int256 c; // c = cos(-phi) >= 0. rounded to 18 decimals + int256 s; // s = sin(-phi) >= 0. rounded to 18 decimals + // Invariant: c^2 + s^2 == 1, i.e., the point (c, s) is normalized. + // Due to rounding, this may not be 1. The term dSq in DerivedParams corrects for this in extra precision + + // Stretching factor: + int256 lambda; // lambda >= 1 where lambda == 1 is the circle. + } + + // terms in this struct are stored in extra precision (38 decimals) with final decimal rounded down + struct DerivedParams { + Vector2 tauAlpha; + Vector2 tauBeta; + int256 u; // from (A chi)_y = lambda * u + v + int256 v; // from (A chi)_y = lambda * u + v + int256 w; // from (A chi)_x = w / lambda + z + int256 z; // from (A chi)_x = w / lambda + z + int256 dSq; // error in c^2 + s^2 = dSq, used to correct errors in c, s, tau, u,v,w,z calculations + } + + struct Vector2 { + int256 x; + int256 y; + } +} diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 741284aa1..6b7391dbd 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -5,22 +5,25 @@ pragma solidity ^0.8.24; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; -import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; -import { BalancerPoolToken } from "@balancer-labs/v3-vault/contracts/BalancerPoolToken.sol"; +import { IGyroECLPPool } from "@balancer-labs/v3-interfaces/contracts/pool-gyro/IGyroECLPPool.sol"; import { ISwapFeePercentageBounds } from "@balancer-labs/v3-interfaces/contracts/vault/ISwapFeePercentageBounds.sol"; import { IUnbalancedLiquidityInvariantRatioBounds } from "@balancer-labs/v3-interfaces/contracts/vault/IUnbalancedLiquidityInvariantRatioBounds.sol"; import { PoolSwapParams, Rounding, SwapKind } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + +import { BalancerPoolToken } from "@balancer-labs/v3-vault/contracts/BalancerPoolToken.sol"; + import "./lib/GyroPoolMath.sol"; import "./lib/GyroECLPMath.sol"; -contract GyroECLPPool is IBasePool, BalancerPoolToken { +contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { using FixedPoint for uint256; using SafeCast for *; @@ -41,24 +44,7 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { int256 internal immutable _dSq; bytes32 private constant _POOL_TYPE = "ECLP"; - struct GyroParams { - string name; - string symbol; - GyroECLPMath.Params eclpParams; - GyroECLPMath.DerivedParams derivedEclpParams; - } - - error SqrtParamsWrong(); - error SupportsOnlyTwoTokens(); - error NotImplemented(); - error AddressIsZeroAddress(); - - event ECLPParamsValidated(bool paramsValidated); - event ECLPDerivedParamsValidated(bool derivedParamsValidated); - event InvariantAterInitializeJoin(uint256 invariantAfterJoin); - event InvariantOldAndNew(uint256 oldInvariant, uint256 newInvariant); - - constructor(GyroParams memory params, IVault vault) BalancerPoolToken(vault, params.name, params.symbol) { + constructor(GyroECLPPoolParams memory params, IVault vault) BalancerPoolToken(vault, params.name, params.symbol) { GyroECLPMath.validateParams(params.eclpParams); emit ECLPParamsValidated(true); @@ -88,10 +74,7 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { /// @inheritdoc IBasePool function computeInvariant(uint256[] memory balancesLiveScaled18, Rounding rounding) public view returns (uint256) { - ( - GyroECLPMath.Params memory eclpParams, - GyroECLPMath.DerivedParams memory derivedECLPParams - ) = _reconstructECLPParams(); + (Params memory eclpParams, DerivedParams memory derivedECLPParams) = _reconstructECLPParams(); (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( balancesLiveScaled18, @@ -112,12 +95,9 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { uint256 tokenInIndex, uint256 invariantRatio ) external view returns (uint256 newBalance) { - ( - GyroECLPMath.Params memory eclpParams, - GyroECLPMath.DerivedParams memory derivedECLPParams - ) = _reconstructECLPParams(); + (Params memory eclpParams, DerivedParams memory derivedECLPParams) = _reconstructECLPParams(); - GyroECLPMath.Vector2 memory invariant; + Vector2 memory invariant; { (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( balancesLiveScaled18, @@ -126,7 +106,7 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { ); // invariant = overestimate in x-component, underestimate in y-component. - invariant = GyroECLPMath.Vector2( + invariant = Vector2( (currentInvariant + 2 * invErr).toUint256().mulUp(invariantRatio).toInt256(), currentInvariant.toUint256().mulUp(invariantRatio).toInt256() ); @@ -149,11 +129,8 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { function onSwap(PoolSwapParams memory request) public view onlyVault returns (uint256) { bool tokenInIsToken0 = request.indexIn == 0; - ( - GyroECLPMath.Params memory eclpParams, - GyroECLPMath.DerivedParams memory derivedECLPParams - ) = _reconstructECLPParams(); - GyroECLPMath.Vector2 memory invariant; + (Params memory eclpParams, DerivedParams memory derivedECLPParams) = _reconstructECLPParams(); + Vector2 memory invariant; { (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( request.balancesScaled18, @@ -162,7 +139,7 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { ); // invariant = overestimate in x-component, underestimate in y-component // No overflow in `+` due to constraints to the different values enforced in GyroECLPMath. - invariant = GyroECLPMath.Vector2(currentInvariant + 2 * invErr, currentInvariant); + invariant = Vector2(currentInvariant + 2 * invErr, currentInvariant); } if (request.kind == SwapKind.EXACT_IN) { @@ -192,11 +169,7 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { } /** @dev reconstructs ECLP params structs from immutable arrays */ - function _reconstructECLPParams() - private - view - returns (GyroECLPMath.Params memory params, GyroECLPMath.DerivedParams memory d) - { + function _reconstructECLPParams() private view returns (Params memory params, DerivedParams memory d) { (params.alpha, params.beta, params.c, params.s, params.lambda) = ( _paramsAlpha, _paramsBeta, @@ -208,11 +181,7 @@ contract GyroECLPPool is IBasePool, BalancerPoolToken { (d.u, d.v, d.w, d.z, d.dSq) = (_u, _v, _w, _z, _dSq); } - function getECLPParams() - external - view - returns (GyroECLPMath.Params memory params, GyroECLPMath.DerivedParams memory d) - { + function getECLPParams() external view returns (Params memory params, DerivedParams memory d) { return _reconstructECLPParams(); } diff --git a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol index ec5875596..47b3c8fbb 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IGyroECLPPool } from "@balancer-labs/v3-interfaces/contracts/pool-gyro/IGyroECLPPool.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; @@ -11,7 +12,6 @@ import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; import { BasePoolFactory } from "@balancer-labs/v3-pool-utils/contracts/BasePoolFactory.sol"; import { GyroECLPPool } from "./GyroECLPPool.sol"; -import { GyroECLPMath } from "./lib/GyroECLPMath.sol"; /** * @notice Gyro ECLP Pool factory @@ -45,8 +45,8 @@ contract GyroECLPPoolFactory is BasePoolFactory { string memory name, string memory symbol, TokenConfig[] memory tokens, - GyroECLPMath.Params memory eclpParams, - GyroECLPMath.DerivedParams memory derivedEclpParams, + IGyroECLPPool.Params memory eclpParams, + IGyroECLPPool.DerivedParams memory derivedEclpParams, PoolRoleAccounts memory roleAccounts, uint256 swapFeePercentage, address poolHooksContract, @@ -58,7 +58,7 @@ contract GyroECLPPoolFactory is BasePoolFactory { pool = _create( abi.encode( - GyroECLPPool.GyroParams({ + IGyroECLPPool.GyroECLPPoolParams({ name: name, symbol: symbol, eclpParams: eclpParams, diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index ffcf45ac6..18014e05f 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -5,12 +5,14 @@ pragma solidity ^0.8.24; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import { IGyroECLPPool } from "@balancer-labs/v3-interfaces/contracts/pool-gyro/IGyroECLPPool.sol"; + import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + import "./SignedFixedPoint.sol"; import "./GyroPoolMath.sol"; -// solhint-disable private-vars-leading-underscore - /** * @notice ECLP math library. Pretty much a direct translation of the python version. * @dev We use *signed* values here because some of the intermediate results can be negative (e.g. coordinates of @@ -18,11 +20,9 @@ import "./GyroPoolMath.sol"; */ library GyroECLPMath { error RotationVectorWrong(); - error PriceBoundsWrong(); error RotationVectorNotNormalized(); error AssetBoundsExceeded(); error DerivedTauNotNormalized(); - error DerivedZetaWrong(); error StretchingFactorWrong(); error DerivedUvwzWrong(); error InvariantDenominatorWrong(); @@ -30,9 +30,9 @@ library GyroECLPMath { error MaxInvariantExceeded(); error DerivedDsqWrong(); - uint256 internal constant ONEHALF = 0.5e18; - int256 internal constant ONE = 1e18; // 18 decimal places - int256 internal constant ONE_XP = 1e38; // 38 decimal places + uint256 internal constant _ONEHALF = 0.5e18; + int256 internal constant _ONE = 1e18; // 18 decimal places + int256 internal constant _ONE_XP = 1e38; // 38 decimal places using SignedFixedPoint for int256; using FixedPoint for uint256; @@ -50,40 +50,6 @@ library GyroECLPMath { int256 internal constant _MAX_BALANCES = 1e34; // 1e16 in normal precision int256 internal constant _MAX_INVARIANT = 3e37; // 3e19 in normal precision - // Note that all t values (not tp or tpp) could consist of uint's, as could all Params. But it's complicated to - // convert all the time, so we make them all signed. We also store all intermediate values signed. An exception are - // the functions that are used by the contract because there the values are stored unsigned. - struct Params { - // Price bounds (lower and upper). 0 < alpha < beta. - int256 alpha; - int256 beta; - // Rotation vector: - // phi in (-90 degrees, 0] is the implicit rotation vector. It's stored as a point: - int256 c; // c = cos(-phi) >= 0. rounded to 18 decimals - int256 s; // s = sin(-phi) >= 0. rounded to 18 decimals - // Invariant: c^2 + s^2 == 1, i.e., the point (c, s) is normalized. - // Due to rounding, this may not be 1. The term dSq in DerivedParams corrects for this in extra precision - - // Stretching factor: - int256 lambda; // lambda >= 1 where lambda == 1 is the circle. - } - - // terms in this struct are stored in extra precision (38 decimals) with final decimal rounded down - struct DerivedParams { - Vector2 tauAlpha; - Vector2 tauBeta; - int256 u; // from (A chi)_y = lambda * u + v - int256 v; // from (A chi)_y = lambda * u + v - int256 w; // from (A chi)_x = w / lambda + z - int256 z; // from (A chi)_x = w / lambda + z - int256 dSq; // error in c^2 + s^2 = dSq, used to correct errors in c, s, tau, u,v,w,z calculations - } - - struct Vector2 { - int256 x; - int256 y; - } - struct QParams { int256 a; int256 b; @@ -91,19 +57,19 @@ library GyroECLPMath { } /// @dev Enforces limits and approximate normalization of the rotation vector. - function validateParams(Params memory params) internal pure { - if (0 > params.s || params.s > ONE) { + function validateParams(IGyroECLPPool.Params memory params) internal pure { + if (0 > params.s || params.s > _ONE) { revert RotationVectorWrong(); } - if (0 > params.c || params.c > ONE) { + if (0 > params.c || params.c > _ONE) { revert RotationVectorWrong(); } - Vector2 memory sc = Vector2(params.s, params.c); + IGyroECLPPool.Vector2 memory sc = IGyroECLPPool.Vector2(params.s, params.c); int256 scnorm2 = scalarProd(sc, sc); // squared norm - if (ONE - _ROTATION_VECTOR_NORM_ACCURACY > scnorm2 || scnorm2 > ONE + _ROTATION_VECTOR_NORM_ACCURACY) { + if (_ONE - _ROTATION_VECTOR_NORM_ACCURACY > scnorm2 || scnorm2 > _ONE + _ROTATION_VECTOR_NORM_ACCURACY) { revert RotationVectorNotNormalized(); } @@ -116,45 +82,55 @@ library GyroECLPMath { * @notice Enforces limits and approximate normalization of the derived values. * @dev Does NOT check for internal consistency of 'derived' with 'params'. */ - function validateDerivedParamsLimits(Params memory params, DerivedParams memory derived) external pure { + function validateDerivedParamsLimits( + IGyroECLPPool.Params memory params, + IGyroECLPPool.DerivedParams memory derived + ) external pure { int256 norm2; norm2 = scalarProdXp(derived.tauAlpha, derived.tauAlpha); - if (ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP > norm2 || norm2 > ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP) { + if (_ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP > norm2 || norm2 > _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP) { revert DerivedTauNotNormalized(); } norm2 = scalarProdXp(derived.tauBeta, derived.tauBeta); - if (ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP > norm2 || norm2 > ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP) { + if (_ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP > norm2 || norm2 > _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP) { revert DerivedTauNotNormalized(); } - if (derived.u > ONE_XP) revert DerivedUvwzWrong(); - if (derived.v > ONE_XP) revert DerivedUvwzWrong(); - if (derived.w > ONE_XP) revert DerivedUvwzWrong(); - if (derived.z > ONE_XP) revert DerivedUvwzWrong(); + if (derived.u > _ONE_XP) revert DerivedUvwzWrong(); + if (derived.v > _ONE_XP) revert DerivedUvwzWrong(); + if (derived.w > _ONE_XP) revert DerivedUvwzWrong(); + if (derived.z > _ONE_XP) revert DerivedUvwzWrong(); if ( - ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP > derived.dSq || derived.dSq > ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP + _ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP > derived.dSq || + derived.dSq > _ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP ) { revert DerivedDsqWrong(); } // NB No anti-overflow checks are required given the checks done above and in validateParams(). - int256 mulDenominator = ONE_XP.divXpU(calcAChiAChiInXp(params, derived) - ONE_XP); + int256 mulDenominator = _ONE_XP.divXpU(calcAChiAChiInXp(params, derived) - _ONE_XP); if (mulDenominator > _MAX_INV_INVARIANT_DENOMINATOR_XP) { revert InvariantDenominatorWrong(); } } - function scalarProd(Vector2 memory t1, Vector2 memory t2) internal pure returns (int256 ret) { + function scalarProd( + IGyroECLPPool.Vector2 memory t1, + IGyroECLPPool.Vector2 memory t2 + ) internal pure returns (int256 ret) { ret = t1.x.mulDownMag(t2.x) + t1.y.mulDownMag(t2.y); } /// @dev Scalar product for extra-precision values - function scalarProdXp(Vector2 memory t1, Vector2 memory t2) internal pure returns (int256 ret) { + function scalarProdXp( + IGyroECLPPool.Vector2 memory t1, + IGyroECLPPool.Vector2 memory t2 + ) internal pure returns (int256 ret) { ret = t1.x.mulXp(t2.x) + t1.y.mulXp(t2.y); } @@ -165,7 +141,10 @@ library GyroECLPMath { * @notice Calculate A t where A is given in Section 2.2. * @dev This is reversing rotation and scaling of the ellipse (mapping back to circle) . */ - function mulA(Params memory params, Vector2 memory tp) internal pure returns (Vector2 memory t) { + function mulA( + IGyroECLPPool.Params memory params, + IGyroECLPPool.Vector2 memory tp + ) internal pure returns (IGyroECLPPool.Vector2 memory t) { // NB: This function is only used inside calculatePrice(). This is why we can make two simplifications: // 1. We don't correct for precision of s, c using d.dSq because that level of precision is not important in // this context; @@ -185,9 +164,9 @@ library GyroECLPMath { * rounding direction is important. */ function virtualOffset0( - Params memory p, - DerivedParams memory d, - Vector2 memory r // overestimate in x component, underestimate in y + IGyroECLPPool.Params memory p, + IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.Vector2 memory r // overestimate in x component, underestimate in y ) internal pure returns (int256 a) { // a = r lambda c tau(beta)_x + rs tau(beta)_y // account for 1 factors of dSq (2 s,c factors) @@ -205,9 +184,9 @@ library GyroECLPMath { * @dev Calculates b = r*(A^{-1}tau(alpha))_y rounding up in signed direction */ function virtualOffset1( - Params memory p, - DerivedParams memory d, - Vector2 memory r // overestimate in x component, underestimate in y + IGyroECLPPool.Params memory p, + IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.Vector2 memory r // overestimate in x component, underestimate in y ) internal pure returns (int256 b) { // b = -r \lambda s tau(alpha)_x + rc tau(alpha)_y // account for 1 factors of dSq (2 s,c factors) @@ -226,9 +205,9 @@ library GyroECLPMath { * by lambda. Rounds down in signed direction */ function maxBalances0( - Params memory p, - DerivedParams memory d, - Vector2 memory r // overestimate in x-component, underestimate in y-component + IGyroECLPPool.Params memory p, + IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.Vector2 memory r // overestimate in x-component, underestimate in y-component ) internal pure returns (int256 xp) { // x^+ = r lambda c (tau(beta)_x - tau(alpha)_x) + rs (tau(beta)_y - tau(alpha)_y) // account for 1 factors of dSq (2 s,c factors) @@ -247,9 +226,9 @@ library GyroECLPMath { * by lambda. Rounds down in signed direction */ function maxBalances1( - Params memory p, - DerivedParams memory d, - Vector2 memory r // overestimate in x-component, underestimate in y-component + IGyroECLPPool.Params memory p, + IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.Vector2 memory r // overestimate in x-component, underestimate in y-component ) internal pure returns (int256 yp) { // y^+ = r lambda s (tau(beta)_x - tau(alpha)_x) + rc (tau(alpha)_y - tau(beta)_y) // account for 1 factors of dSq (2 s,c factors) @@ -268,8 +247,8 @@ library GyroECLPMath { */ function calculateInvariantWithError( uint256[] memory balances, - Params memory params, - DerivedParams memory derived + IGyroECLPPool.Params memory params, + IGyroECLPPool.DerivedParams memory derived ) public pure returns (int256, int256) { (int256 x, int256 y) = (balances[0].toInt256(), balances[1].toInt256()); @@ -292,11 +271,11 @@ library GyroECLPMath { err = err > 0 ? GyroPoolMath.sqrt(err.toUint256(), 5).toInt256() : int256(1e9); } // Calculate the error in the numerator, scale the error by 20 to be sure all possible terms accounted for - err = ((params.lambda.mulUpMagU(x + y) / ONE_XP) + err + 1) * 20; + err = ((params.lambda.mulUpMagU(x + y) / _ONE_XP) + err + 1) * 20; // A chi \cdot A chi > 1, so round it up to round denominator up. // Denominator uses extra precision, so we do * 1/denominator so we are sure the calculation doesn't overflow. - int256 mulDenominator = ONE_XP.divXpU(calcAChiAChiInXp(params, derived) - ONE_XP); + int256 mulDenominator = _ONE_XP.divXpU(calcAChiAChiInXp(params, derived) - _ONE_XP); // NOTE: Anti-overflow limits on mulDenominator are checked on contract creation. // As alternative, could do, but could overflow: invariant = (AtAChi.add(sqrt) - err).divXp(denominator); @@ -315,7 +294,7 @@ library GyroECLPMath { err = err + ((invariant.mulUpXpToNpU(mulDenominator) * ((params.lambda * params.lambda) / 1e36)) * 40) / - ONE_XP + + _ONE_XP + 1; if (invariant + err > _MAX_INVARIANT) { @@ -329,8 +308,8 @@ library GyroECLPMath { function calcAtAChi( int256 x, int256 y, - Params memory p, - DerivedParams memory d + IGyroECLPPool.Params memory p, + IGyroECLPPool.DerivedParams memory d ) internal pure returns (int256 val) { // to save gas, pre-compute dSq^2 as it will be used 3 times int256 dSq2 = d.dSq.mulXpU(d.dSq); @@ -355,7 +334,10 @@ library GyroECLPMath { * because it will be at most 38 + 16 digits (38 from decimals, 2*8 from lambda^2 if lambda=1e8). Since we will * only divide by this later, we will not need to worry about overflow in that operation if done in the right way. */ - function calcAChiAChiInXp(Params memory p, DerivedParams memory d) internal pure returns (int256 val) { + function calcAChiAChiInXp( + IGyroECLPPool.Params memory p, + IGyroECLPPool.DerivedParams memory d + ) internal pure returns (int256 val) { // To save gas, pre-compute dSq^3 as it will be used 4 times. int256 dSq3 = d.dSq.mulXpU(d.dSq).mulXpU(d.dSq); @@ -382,8 +364,8 @@ library GyroECLPMath { function calcMinAtxAChiySqPlusAtxSq( int256 x, int256 y, - Params memory p, - DerivedParams memory d + IGyroECLPPool.Params memory p, + IGyroECLPPool.DerivedParams memory d ) internal pure returns (int256 val) { //////////////////////////////////////////////////////////////////////////////////// // (At)_x^2 (A chi)_y^2 = (x^2 c^2 - xy2sc + y^2 s^2) (u^2 + 2uv/lambda + v^2/lambda^2) @@ -417,8 +399,8 @@ library GyroECLPMath { function calc2AtxAtyAChixAChiy( int256 x, int256 y, - Params memory p, - DerivedParams memory d + IGyroECLPPool.Params memory p, + IGyroECLPPool.DerivedParams memory d ) internal pure returns (int256 val) { //////////////////////////////////////////////////////////////////////////////////// // = ((x^2 - y^2)sc + yx(c^2-s^2)) * 2 * (zu + (wu + zv)/lambda + wv/lambda^2) @@ -439,8 +421,8 @@ library GyroECLPMath { function calcMinAtyAChixSqPlusAtySq( int256 x, int256 y, - Params memory p, - DerivedParams memory d + IGyroECLPPool.Params memory p, + IGyroECLPPool.DerivedParams memory d ) internal pure returns (int256 val) { //////////////////////////////////////////////////////////////////////////////////// // (At)_y^2 (A chi)_x^2 = (x^2 s^2 + xy2sc + y^2 c^2) * (z^2 + 2zw/lambda + w^2/lambda^2) @@ -469,8 +451,8 @@ library GyroECLPMath { function calcInvariantSqrt( int256 x, int256 y, - Params memory p, - DerivedParams memory d + IGyroECLPPool.Params memory p, + IGyroECLPPool.DerivedParams memory d ) internal pure returns (int256 val, int256 err) { val = calcMinAtxAChiySqPlusAtxSq(x, y, p, d) + calc2AtxAtyAChixAChiy(x, y, p, d); val = val + calcMinAtyAChixSqPlusAtySq(x, y, p, d); @@ -492,27 +474,33 @@ library GyroECLPMath { */ function calcSpotPrice0in1( uint256[] memory balances, - Params memory params, - DerivedParams memory derived, + IGyroECLPPool.Params memory params, + IGyroECLPPool.DerivedParams memory derived, int256 invariant ) external pure returns (uint256 px) { // Shift by virtual offsets to get v(t). // Ignore r rounding for spot price, precision will be lost in TWAP anyway. - Vector2 memory r = Vector2(invariant, invariant); - Vector2 memory ab = Vector2(virtualOffset0(params, derived, r), virtualOffset1(params, derived, r)); - Vector2 memory vec = Vector2(balances[0].toInt256() - ab.x, balances[1].toInt256() - ab.y); + IGyroECLPPool.Vector2 memory r = IGyroECLPPool.Vector2(invariant, invariant); + IGyroECLPPool.Vector2 memory ab = IGyroECLPPool.Vector2( + virtualOffset0(params, derived, r), + virtualOffset1(params, derived, r) + ); + IGyroECLPPool.Vector2 memory vec = IGyroECLPPool.Vector2( + balances[0].toInt256() - ab.x, + balances[1].toInt256() - ab.y + ); // Transform to circle to get Av(t). vec = mulA(params, vec); // Compute prices on circle. - Vector2 memory pc = Vector2(vec.x.divDownMagU(vec.y), ONE); + IGyroECLPPool.Vector2 memory pc = IGyroECLPPool.Vector2(vec.x.divDownMagU(vec.y), _ONE); // Convert prices back to ellipse // NB: These operations check for overflow because the price pc[0] might be large when vex.y is small. // SOMEDAY I think this probably can't actually happen due to our bounds on the different values. In this case // we could do this unchecked as well. - int256 pgx = scalarProd(pc, mulA(params, Vector2(ONE, 0))); - px = pgx.divDownMag(scalarProd(pc, mulA(params, Vector2(0, ONE)))).toUint256(); + int256 pgx = scalarProd(pc, mulA(params, IGyroECLPPool.Vector2(_ONE, 0))); + px = pgx.divDownMag(scalarProd(pc, mulA(params, IGyroECLPPool.Vector2(0, _ONE)))).toUint256(); } /** @@ -521,9 +509,9 @@ library GyroECLPMath { * (0 = X, 1 = Y) */ function checkAssetBounds( - Params memory params, - DerivedParams memory derived, - Vector2 memory invariant, + IGyroECLPPool.Params memory params, + IGyroECLPPool.DerivedParams memory derived, + IGyroECLPPool.Vector2 memory invariant, int256 newBal, uint8 assetIndex ) internal pure { @@ -542,11 +530,13 @@ library GyroECLPMath { uint256[] memory balances, uint256 amountIn, bool tokenInIsToken0, - Params memory params, - DerivedParams memory derived, - Vector2 memory invariant + IGyroECLPPool.Params memory params, + IGyroECLPPool.DerivedParams memory derived, + IGyroECLPPool.Vector2 memory invariant ) external pure returns (uint256 amountOut) { - function(int256, Params memory, DerivedParams memory, Vector2 memory) pure returns (int256) calcGiven; + function(int256, IGyroECLPPool.Params memory, IGyroECLPPool.DerivedParams memory, IGyroECLPPool.Vector2 memory) + pure + returns (int256) calcGiven; uint8 ixIn; uint8 ixOut; if (tokenInIsToken0) { @@ -571,11 +561,13 @@ library GyroECLPMath { uint256[] memory balances, uint256 amountOut, bool tokenInIsToken0, - Params memory params, - DerivedParams memory derived, - Vector2 memory invariant + IGyroECLPPool.Params memory params, + IGyroECLPPool.DerivedParams memory derived, + IGyroECLPPool.Vector2 memory invariant ) external pure returns (uint256 amountIn) { - function(int256, Params memory, DerivedParams memory, Vector2 memory) pure returns (int256) calcGiven; + function(int256, IGyroECLPPool.Params memory, IGyroECLPPool.DerivedParams memory, IGyroECLPPool.Vector2 memory) + pure + returns (int256) calcGiven; uint8 ixIn; uint8 ixOut; if (tokenInIsToken0) { @@ -607,13 +599,13 @@ library GyroECLPMath { int256 x, int256 s, int256 c, - Vector2 memory r, // overestimate in x component, underestimate in y - Vector2 memory ab, - Vector2 memory tauBeta, + IGyroECLPPool.Vector2 memory r, // overestimate in x component, underestimate in y + IGyroECLPPool.Vector2 memory ab, + IGyroECLPPool.Vector2 memory tauBeta, int256 dSq ) internal pure returns (int256) { // x component will round up, y will round down, use extra precision. - Vector2 memory lamBar; + IGyroECLPPool.Vector2 memory lamBar; lamBar.x = SignedFixedPoint.ONE_XP - SignedFixedPoint.ONE_XP.divDownMagU(lambda).divDownMagU(lambda); // Note: The following cannot become negative even with errors because we require lambda >= 1 and divUpMag // returns the exact result if the quotient is representable in 18 decimals. @@ -632,12 +624,12 @@ library GyroECLPMath { // x component will round up, y will round down, use extra precision. // Account for 1 factor of dSq (2 s,c factors). - Vector2 memory sTerm; + IGyroECLPPool.Vector2 memory sTerm; // We wil take sTerm = 1 - sTerm below, using multiple lines to avoid "stack too deep". sTerm.x = lamBar.y.mulDownMagU(s).mulDownMagU(s).divXpU(dSq); sTerm.y = lamBar.x.mulUpMagU(s); sTerm.y = sTerm.y.mulUpMagU(s).divXpU(dSq + 1) + 1; // account for rounding error in dSq, divXp - sTerm = Vector2(SignedFixedPoint.ONE_XP - sTerm.x, SignedFixedPoint.ONE_XP - sTerm.y); + sTerm = IGyroECLPPool.Vector2(SignedFixedPoint.ONE_XP - sTerm.x, SignedFixedPoint.ONE_XP - sTerm.y); // ^^ NB: The components of sTerm are non-negative: We only need to worry about sTerm.y. This is non-negative // because, because of bounds on lambda lamBar <= 1 - 1e-16, and division by dSq ensures we have enough // precision so that rounding errors are never magnitude 1e-16. @@ -667,11 +659,11 @@ library GyroECLPMath { */ function calcXpXpDivLambdaLambda( int256 x, - Vector2 memory r, // overestimate in x component, underestimate in y + IGyroECLPPool.Vector2 memory r, // overestimate in x component, underestimate in y int256 lambda, int256 s, int256 c, - Vector2 memory tauBeta, + IGyroECLPPool.Vector2 memory tauBeta, int256 dSq ) internal pure returns (int256) { ////////////////////////////////////////////////////////////////////////////////// @@ -681,7 +673,7 @@ library GyroECLPMath { ////////////////////////////////////////////////////////////////////////////////// // to save gas, pre-compute dSq^2 as it will be used 3 times, and r.x^2 as it will be used 2-3 times // sqVars = (dSq^2, r.x^2) - Vector2 memory sqVars = Vector2(dSq.mulXpU(dSq), r.x.mulUpMagU(r.x)); + IGyroECLPPool.Vector2 memory sqVars = IGyroECLPPool.Vector2(dSq.mulXpU(dSq), r.x.mulUpMagU(r.x)); QParams memory q; // for working terms // q.a = r^2 s 2c tau(beta)_x tau(beta)_y @@ -737,27 +729,33 @@ library GyroECLPMath { */ function calcYGivenX( int256 x, - Params memory params, - DerivedParams memory d, - Vector2 memory r // overestimate in x component, underestimate in y + IGyroECLPPool.Params memory params, + IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.Vector2 memory r // overestimate in x component, underestimate in y ) internal pure returns (int256 y) { // Want to overestimate the virtual offsets except in a particular setting that will be corrected for later. // Note that the error correction in the invariant should more than make up for uncaught rounding directions // (in 38 decimals) in virtual offsets. - Vector2 memory ab = Vector2(virtualOffset0(params, d, r), virtualOffset1(params, d, r)); + IGyroECLPPool.Vector2 memory ab = IGyroECLPPool.Vector2( + virtualOffset0(params, d, r), + virtualOffset1(params, d, r) + ); y = solveQuadraticSwap(params.lambda, x, params.s, params.c, r, ab, d.tauBeta, d.dSq); } function calcXGivenY( int256 y, - Params memory params, - DerivedParams memory d, - Vector2 memory r // overestimate in x component, underestimate in y + IGyroECLPPool.Params memory params, + IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.Vector2 memory r // overestimate in x component, underestimate in y ) internal pure returns (int256 x) { // Want to overestimate the virtual offsets except in a particular setting that will be corrected for later. // Note that the error correction in the invariant should more than make up for uncaught rounding directions // (in 38 decimals) in virtual offsets. - Vector2 memory ba = Vector2(virtualOffset1(params, d, r), virtualOffset0(params, d, r)); + IGyroECLPPool.Vector2 memory ba = IGyroECLPPool.Vector2( + virtualOffset1(params, d, r), + virtualOffset0(params, d, r) + ); // Change x->y, s->c, c->s, b->a, a->b, tauBeta.x -> -tauAlpha.x, tauBeta.y -> tauAlpha.y vs calcYGivenX. x = solveQuadraticSwap( params.lambda, @@ -766,7 +764,7 @@ library GyroECLPMath { params.s, r, ba, - Vector2(-d.tauAlpha.x, d.tauAlpha.y), + IGyroECLPPool.Vector2(-d.tauAlpha.x, d.tauAlpha.y), d.dSq ); } diff --git a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol index 045ac9873..9ea88cb31 100644 --- a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol +++ b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol @@ -4,21 +4,21 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; +import { IGyroECLPPool } from "@balancer-labs/v3-interfaces/contracts/pool-gyro/IGyroECLPPool.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; -import { PoolRoleAccounts } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; import { IVaultMock } from "@balancer-labs/v3-interfaces/contracts/test/IVaultMock.sol"; -import { TokenConfig } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { PoolRoleAccounts, TokenConfig } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { BaseContractsDeployer } from "@balancer-labs/v3-solidity-utils/test/foundry/utils/BaseContractsDeployer.sol"; import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; import { ProtocolFeeControllerMock } from "@balancer-labs/v3-vault/contracts/test/ProtocolFeeControllerMock.sol"; import { GyroECLPPoolFactory } from "../../../contracts/GyroECLPPoolFactory.sol"; import { GyroECLPPool } from "../../../contracts/GyroECLPPool.sol"; -import { GyroECLPMath } from "../../../contracts/lib/GyroECLPMath.sol"; -contract GyroEclpPoolDeployer is Test { +contract GyroEclpPoolDeployer is BaseContractsDeployer { using CastingHelpers for address[]; // Extracted from pool 0x2191df821c198600499aa1f0031b1a7514d7a7d9 on Mainnet. @@ -38,6 +38,15 @@ contract GyroEclpPoolDeployer is Test { int256 internal _z = -28859471639991253843240999485797747790; int256 internal _dSq = 99999999999999999886624093342106115200; + string private artifactsRootDir = "artifacts/"; + + constructor() { + // If this external artifact path exists, it means we are running outside of this repo. + if (vm.exists("artifacts/@balancer-labs/v3-pool-gyro/")) { + artifactsRootDir = "artifacts/@balancer-labs/v3-pool-gyro/"; + } + } + function createGyroEclpPool( address[] memory tokens, IRateProvider[] memory rateProviders, @@ -45,14 +54,14 @@ contract GyroEclpPoolDeployer is Test { IVaultMock vault, address poolCreator ) internal returns (address) { - GyroECLPPoolFactory factory = new GyroECLPPoolFactory(IVault(address(vault)), 365 days); + GyroECLPPoolFactory factory = deployGyro2CLPPoolFactory(vault); PoolRoleAccounts memory roleAccounts; GyroECLPPool newPool; // Avoids Stack-too-deep. { - GyroECLPMath.Params memory params = GyroECLPMath.Params({ + IGyroECLPPool.Params memory params = IGyroECLPPool.Params({ alpha: _paramsAlpha, beta: _paramsBeta, c: _paramsC, @@ -60,9 +69,9 @@ contract GyroEclpPoolDeployer is Test { lambda: _paramsLambda }); - GyroECLPMath.DerivedParams memory derivedParams = GyroECLPMath.DerivedParams({ - tauAlpha: GyroECLPMath.Vector2(_tauAlphaX, _tauAlphaY), - tauBeta: GyroECLPMath.Vector2(_tauBetaX, _tauBetaY), + IGyroECLPPool.DerivedParams memory derivedParams = IGyroECLPPool.DerivedParams({ + tauAlpha: IGyroECLPPool.Vector2(_tauAlphaX, _tauAlphaY), + tauBeta: IGyroECLPPool.Vector2(_tauBetaX, _tauBetaY), u: _u, v: _v, w: _w, @@ -96,4 +105,19 @@ contract GyroEclpPoolDeployer is Test { return address(newPool); } + + function deployGyro2CLPPoolFactory(IVault vault) internal returns (GyroECLPPoolFactory) { + if (reusingArtifacts) { + return + GyroECLPPoolFactory( + deployCode(_computeGyroECLPPath(type(GyroECLPPoolFactory).name), abi.encode(vault, 365 days)) + ); + } else { + return new GyroECLPPoolFactory(vault, 365 days); + } + } + + function _computeGyroECLPPath(string memory name) private view returns (string memory) { + return string(abi.encodePacked(artifactsRootDir, "contracts/", name, ".sol/", name, ".json")); + } } From 54276b69423eb7bea4acc726f903d00f5f96b846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 30 Oct 2024 12:31:22 -0300 Subject: [PATCH 33/52] Fix Gyro ECLP deployment --- pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol | 5 +++++ pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol index 47b3c8fbb..2cbed2c53 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol @@ -20,6 +20,7 @@ import { GyroECLPPool } from "./GyroECLPPool.sol"; contract GyroECLPPoolFactory is BasePoolFactory { // solhint-disable not-rely-on-time + /// @notice ECLP pools support 2 tokens only. error SupportsOnlyTwoTokens(); constructor( @@ -56,6 +57,10 @@ contract GyroECLPPoolFactory is BasePoolFactory { revert SupportsOnlyTwoTokens(); } + if (roleAccounts.poolCreator != address(0)) { + revert StandardPoolWithCreator(); + } + pool = _create( abi.encode( IGyroECLPPool.GyroECLPPoolParams({ diff --git a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol index 9ea88cb31..34a97b650 100644 --- a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol +++ b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol @@ -54,7 +54,7 @@ contract GyroEclpPoolDeployer is BaseContractsDeployer { IVaultMock vault, address poolCreator ) internal returns (address) { - GyroECLPPoolFactory factory = deployGyro2CLPPoolFactory(vault); + GyroECLPPoolFactory factory = deployGyroECLPPoolFactory(vault); PoolRoleAccounts memory roleAccounts; GyroECLPPool newPool; @@ -106,7 +106,7 @@ contract GyroEclpPoolDeployer is BaseContractsDeployer { return address(newPool); } - function deployGyro2CLPPoolFactory(IVault vault) internal returns (GyroECLPPoolFactory) { + function deployGyroECLPPoolFactory(IVault vault) internal returns (GyroECLPPoolFactory) { if (reusingArtifacts) { return GyroECLPPoolFactory( From b89427db79b04960c4d26c307b4cd996fccf4bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 30 Oct 2024 15:05:41 -0300 Subject: [PATCH 34/52] Fix hardhat compilation --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 3 +-- pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 22 ++++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 6b7391dbd..28a65357a 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -20,8 +20,7 @@ import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/Fixe import { BalancerPoolToken } from "@balancer-labs/v3-vault/contracts/BalancerPoolToken.sol"; -import "./lib/GyroPoolMath.sol"; -import "./lib/GyroECLPMath.sol"; +import { GyroECLPMath } from "./lib/GyroECLPMath.sol"; contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { using FixedPoint for uint256; diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index 18014e05f..c09ff9114 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -10,8 +10,8 @@ import { IGyroECLPPool } from "@balancer-labs/v3-interfaces/contracts/pool-gyro/ import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; -import "./SignedFixedPoint.sol"; -import "./GyroPoolMath.sol"; +import { SignedFixedPoint } from "./SignedFixedPoint.sol"; +import { GyroPoolMath } from "./GyroPoolMath.sol"; /** * @notice ECLP math library. Pretty much a direct translation of the python version. @@ -19,6 +19,11 @@ import "./GyroPoolMath.sol"; * points in the untransformed circle, "prices" in the untransformed circle). */ library GyroECLPMath { + using SignedFixedPoint for int256; + using FixedPoint for uint256; + using SafeCast for uint256; + using SafeCast for int256; + error RotationVectorWrong(); error RotationVectorNotNormalized(); error AssetBoundsExceeded(); @@ -34,11 +39,6 @@ library GyroECLPMath { int256 internal constant _ONE = 1e18; // 18 decimal places int256 internal constant _ONE_XP = 1e38; // 38 decimal places - using SignedFixedPoint for int256; - using FixedPoint for uint256; - using SafeCast for uint256; - using SafeCast for int256; - // Anti-overflow limits: Params and DerivedParams (static, only needs to be checked on pool creation). int256 internal constant _ROTATION_VECTOR_NORM_ACCURACY = 1e3; // 1e-15 in normal precision int256 internal constant _MAX_STRETCH_FACTOR = 1e26; // 1e8 in normal precision @@ -85,7 +85,7 @@ library GyroECLPMath { function validateDerivedParamsLimits( IGyroECLPPool.Params memory params, IGyroECLPPool.DerivedParams memory derived - ) external pure { + ) internal pure { int256 norm2; norm2 = scalarProdXp(derived.tauAlpha, derived.tauAlpha); @@ -477,7 +477,7 @@ library GyroECLPMath { IGyroECLPPool.Params memory params, IGyroECLPPool.DerivedParams memory derived, int256 invariant - ) external pure returns (uint256 px) { + ) internal pure returns (uint256 px) { // Shift by virtual offsets to get v(t). // Ignore r rounding for spot price, precision will be lost in TWAP anyway. IGyroECLPPool.Vector2 memory r = IGyroECLPPool.Vector2(invariant, invariant); @@ -533,7 +533,7 @@ library GyroECLPMath { IGyroECLPPool.Params memory params, IGyroECLPPool.DerivedParams memory derived, IGyroECLPPool.Vector2 memory invariant - ) external pure returns (uint256 amountOut) { + ) internal pure returns (uint256 amountOut) { function(int256, IGyroECLPPool.Params memory, IGyroECLPPool.DerivedParams memory, IGyroECLPPool.Vector2 memory) pure returns (int256) calcGiven; @@ -564,7 +564,7 @@ library GyroECLPMath { IGyroECLPPool.Params memory params, IGyroECLPPool.DerivedParams memory derived, IGyroECLPPool.Vector2 memory invariant - ) external pure returns (uint256 amountIn) { + ) internal pure returns (uint256 amountIn) { function(int256, IGyroECLPPool.Params memory, IGyroECLPPool.DerivedParams memory, IGyroECLPPool.Vector2 memory) pure returns (int256) calcGiven; From d801af90ee3e22786b67efbb5bd3de324aeee193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 30 Oct 2024 15:33:37 -0300 Subject: [PATCH 35/52] Fix gyro ECLP build on hardhat --- pkg/pool-gyro/contracts/Gyro2CLPPool.sol | 7 +++++-- pkg/pool-gyro/contracts/GyroECLPPool.sol | 7 +++++-- pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/pool-gyro/contracts/Gyro2CLPPool.sol b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol index 7b3bd8b1e..e386b99a2 100644 --- a/pkg/pool-gyro/contracts/Gyro2CLPPool.sol +++ b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol @@ -42,7 +42,10 @@ contract Gyro2CLPPool is IGyro2CLPPool, BalancerPoolToken { } /// @inheritdoc IBasePool - function computeInvariant(uint256[] memory balancesLiveScaled18, Rounding rounding) public view returns (uint256) { + function computeInvariant( + uint256[] memory balancesLiveScaled18, + Rounding rounding + ) external view returns (uint256) { (uint256 sqrtAlpha, uint256 sqrtBeta) = _getSqrtAlphaAndBeta(); return Gyro2CLPMath.calculateInvariant(balancesLiveScaled18, sqrtAlpha, sqrtBeta, rounding); @@ -104,7 +107,7 @@ contract Gyro2CLPPool is IGyro2CLPPool, BalancerPoolToken { } /// @inheritdoc IBasePool - function onSwap(PoolSwapParams calldata request) public view onlyVault returns (uint256) { + function onSwap(PoolSwapParams calldata request) external view onlyVault returns (uint256) { bool tokenInIsToken0 = request.indexIn == 0; uint256 balanceTokenInScaled18 = request.balancesScaled18[request.indexIn]; uint256 balanceTokenOutScaled18 = request.balancesScaled18[request.indexOut]; diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 28a65357a..bef5bc9e7 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -72,7 +72,10 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { } /// @inheritdoc IBasePool - function computeInvariant(uint256[] memory balancesLiveScaled18, Rounding rounding) public view returns (uint256) { + function computeInvariant( + uint256[] memory balancesLiveScaled18, + Rounding rounding + ) external view returns (uint256) { (Params memory eclpParams, DerivedParams memory derivedECLPParams) = _reconstructECLPParams(); (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( @@ -125,7 +128,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { } /// @inheritdoc IBasePool - function onSwap(PoolSwapParams memory request) public view onlyVault returns (uint256) { + function onSwap(PoolSwapParams memory request) external view onlyVault returns (uint256) { bool tokenInIsToken0 = request.indexIn == 0; (Params memory eclpParams, DerivedParams memory derivedECLPParams) = _reconstructECLPParams(); diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index c09ff9114..5d0173511 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -249,7 +249,7 @@ library GyroECLPMath { uint256[] memory balances, IGyroECLPPool.Params memory params, IGyroECLPPool.DerivedParams memory derived - ) public pure returns (int256, int256) { + ) internal pure returns (int256, int256) { (int256 x, int256 y) = (balances[0].toInt256(), balances[1].toInt256()); if (x + y > _MAX_BALANCES) { From 582b2ea890c52ddc8377b85967531a8071ca8bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 30 Oct 2024 16:20:11 -0300 Subject: [PATCH 36/52] Improve documentation --- .../contracts/pool-gyro/IGyroECLPPool.sol | 58 +++++++----- pkg/pool-gyro/contracts/GyroECLPPool.sol | 10 +- .../contracts/GyroECLPPoolFactory.sol | 4 +- pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 94 ++++++++++--------- .../foundry/utils/GyroEclpPoolDeployer.sol | 4 +- 5 files changed, 93 insertions(+), 77 deletions(-) diff --git a/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol b/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol index 66b0f6c3d..7d3b03510 100644 --- a/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol +++ b/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol @@ -9,48 +9,58 @@ interface IGyroECLPPool is IBasePool { event ECLPDerivedParamsValidated(bool derivedParamsValidated); /** - * @notice Gyro 2CLP pool configuration. + * @notice Gyro ECLP pool configuration. * @param name Pool name * @param symbol Pool symbol - * @param sqrtAlpha Square root of alpha (the lowest price in the price interval of the 2CLP price curve) - * @param sqrtBeta Square root of beta (the highest price in the price interval of the 2CLP price curve) + * @param eclpParams Parameters to configure the E-CLP pool, with 18 decimals + * @param derivedEclpParams Parameters calculated off-chain based on eclpParams. 38 decimals for higher precision */ struct GyroECLPPoolParams { string name; string symbol; - Params eclpParams; - DerivedParams derivedEclpParams; + EclpParams eclpParams; + DerivedEclpParams derivedEclpParams; } - // Note that all t values (not tp or tpp) could consist of uint's, as could all Params. But it's complicated to - // convert all the time, so we make them all signed. We also store all intermediate values signed. An exception are - // the functions that are used by the contract because there the values are stored unsigned. - struct Params { - // Price bounds (lower and upper). 0 < alpha < beta. + /** + * @notice Struct containing parameters to build the ellipse which describes the pricing curve of an E-CLP pool. + * @dev Note that all values are positive and could consist of uint's. However, this would require converting to + * int numerous times because of int operations, so we store them as int to simplify the code. + * + * @param alpha Lower price limit. alpha > 0 + * @param beta Upper price limit. beta > alpha > 0 + * @param c `c = cos(-phi) >= 0`, rounded to 18 decimals. Phi is the rotation angle of the ellipse + * @param s `s = sin(-phi) >= 0`, rounded to 18 decimals. Phi is the rotation angle of the ellipse + * @param lambda Stretching factor, lambda >= 1. When lambda == 1, we have a perfect circle + */ + struct EclpParams { int256 alpha; int256 beta; - // Rotation vector: - // phi in (-90 degrees, 0] is the implicit rotation vector. It's stored as a point: - int256 c; // c = cos(-phi) >= 0. rounded to 18 decimals - int256 s; // s = sin(-phi) >= 0. rounded to 18 decimals + int256 c; + int256 s; // Invariant: c^2 + s^2 == 1, i.e., the point (c, s) is normalized. // Due to rounding, this may not be 1. The term dSq in DerivedParams corrects for this in extra precision - - // Stretching factor: - int256 lambda; // lambda >= 1 where lambda == 1 is the circle. + int256 lambda; } - // terms in this struct are stored in extra precision (38 decimals) with final decimal rounded down - struct DerivedParams { + /** + * @notice Struct containing parameters calculated based on EclpParams, off-chain. + * @dev All these parameters can be calculated using the EclpParams, but they're calculated off-chain to save gas + * and increase the precision. Therefore, the numbers are stored with 38 decimals precision. Please refer to + * https://docs.gyro.finance/gyroscope-protocol/technical-documents, document "E-CLP high-precision + * calculations.pdf", for further explanations on how to obtain the parameters below. + */ + struct DerivedEclpParams { Vector2 tauAlpha; Vector2 tauBeta; - int256 u; // from (A chi)_y = lambda * u + v - int256 v; // from (A chi)_y = lambda * u + v - int256 w; // from (A chi)_x = w / lambda + z - int256 z; // from (A chi)_x = w / lambda + z - int256 dSq; // error in c^2 + s^2 = dSq, used to correct errors in c, s, tau, u,v,w,z calculations + int256 u; + int256 v; + int256 w; + int256 z; + int256 dSq; } + /// @notice Struct containing a 2D vector. struct Vector2 { int256 x; int256 y; diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index bef5bc9e7..99bd50b02 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -76,7 +76,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { uint256[] memory balancesLiveScaled18, Rounding rounding ) external view returns (uint256) { - (Params memory eclpParams, DerivedParams memory derivedECLPParams) = _reconstructECLPParams(); + (EclpParams memory eclpParams, DerivedEclpParams memory derivedECLPParams) = _reconstructECLPParams(); (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( balancesLiveScaled18, @@ -97,7 +97,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { uint256 tokenInIndex, uint256 invariantRatio ) external view returns (uint256 newBalance) { - (Params memory eclpParams, DerivedParams memory derivedECLPParams) = _reconstructECLPParams(); + (EclpParams memory eclpParams, DerivedEclpParams memory derivedECLPParams) = _reconstructECLPParams(); Vector2 memory invariant; { @@ -131,7 +131,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { function onSwap(PoolSwapParams memory request) external view onlyVault returns (uint256) { bool tokenInIsToken0 = request.indexIn == 0; - (Params memory eclpParams, DerivedParams memory derivedECLPParams) = _reconstructECLPParams(); + (EclpParams memory eclpParams, DerivedEclpParams memory derivedECLPParams) = _reconstructECLPParams(); Vector2 memory invariant; { (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( @@ -171,7 +171,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { } /** @dev reconstructs ECLP params structs from immutable arrays */ - function _reconstructECLPParams() private view returns (Params memory params, DerivedParams memory d) { + function _reconstructECLPParams() private view returns (EclpParams memory params, DerivedEclpParams memory d) { (params.alpha, params.beta, params.c, params.s, params.lambda) = ( _paramsAlpha, _paramsBeta, @@ -183,7 +183,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { (d.u, d.v, d.w, d.z, d.dSq) = (_u, _v, _w, _z, _dSq); } - function getECLPParams() external view returns (Params memory params, DerivedParams memory d) { + function getECLPParams() external view returns (EclpParams memory params, DerivedEclpParams memory d) { return _reconstructECLPParams(); } diff --git a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol index 2cbed2c53..85e50e511 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol @@ -46,8 +46,8 @@ contract GyroECLPPoolFactory is BasePoolFactory { string memory name, string memory symbol, TokenConfig[] memory tokens, - IGyroECLPPool.Params memory eclpParams, - IGyroECLPPool.DerivedParams memory derivedEclpParams, + IGyroECLPPool.EclpParams memory eclpParams, + IGyroECLPPool.DerivedEclpParams memory derivedEclpParams, PoolRoleAccounts memory roleAccounts, uint256 swapFeePercentage, address poolHooksContract, diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index 5d0173511..2ec0ab54c 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -57,7 +57,7 @@ library GyroECLPMath { } /// @dev Enforces limits and approximate normalization of the rotation vector. - function validateParams(IGyroECLPPool.Params memory params) internal pure { + function validateParams(IGyroECLPPool.EclpParams memory params) internal pure { if (0 > params.s || params.s > _ONE) { revert RotationVectorWrong(); } @@ -83,8 +83,8 @@ library GyroECLPMath { * @dev Does NOT check for internal consistency of 'derived' with 'params'. */ function validateDerivedParamsLimits( - IGyroECLPPool.Params memory params, - IGyroECLPPool.DerivedParams memory derived + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived ) internal pure { int256 norm2; norm2 = scalarProdXp(derived.tauAlpha, derived.tauAlpha); @@ -142,7 +142,7 @@ library GyroECLPMath { * @dev This is reversing rotation and scaling of the ellipse (mapping back to circle) . */ function mulA( - IGyroECLPPool.Params memory params, + IGyroECLPPool.EclpParams memory params, IGyroECLPPool.Vector2 memory tp ) internal pure returns (IGyroECLPPool.Vector2 memory t) { // NB: This function is only used inside calculatePrice(). This is why we can make two simplifications: @@ -164,8 +164,8 @@ library GyroECLPMath { * rounding direction is important. */ function virtualOffset0( - IGyroECLPPool.Params memory p, - IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.EclpParams memory p, + IGyroECLPPool.DerivedEclpParams memory d, IGyroECLPPool.Vector2 memory r // overestimate in x component, underestimate in y ) internal pure returns (int256 a) { // a = r lambda c tau(beta)_x + rs tau(beta)_y @@ -184,8 +184,8 @@ library GyroECLPMath { * @dev Calculates b = r*(A^{-1}tau(alpha))_y rounding up in signed direction */ function virtualOffset1( - IGyroECLPPool.Params memory p, - IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.EclpParams memory p, + IGyroECLPPool.DerivedEclpParams memory d, IGyroECLPPool.Vector2 memory r // overestimate in x component, underestimate in y ) internal pure returns (int256 b) { // b = -r \lambda s tau(alpha)_x + rc tau(alpha)_y @@ -205,8 +205,8 @@ library GyroECLPMath { * by lambda. Rounds down in signed direction */ function maxBalances0( - IGyroECLPPool.Params memory p, - IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.EclpParams memory p, + IGyroECLPPool.DerivedEclpParams memory d, IGyroECLPPool.Vector2 memory r // overestimate in x-component, underestimate in y-component ) internal pure returns (int256 xp) { // x^+ = r lambda c (tau(beta)_x - tau(alpha)_x) + rs (tau(beta)_y - tau(alpha)_y) @@ -226,8 +226,8 @@ library GyroECLPMath { * by lambda. Rounds down in signed direction */ function maxBalances1( - IGyroECLPPool.Params memory p, - IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.EclpParams memory p, + IGyroECLPPool.DerivedEclpParams memory d, IGyroECLPPool.Vector2 memory r // overestimate in x-component, underestimate in y-component ) internal pure returns (int256 yp) { // y^+ = r lambda s (tau(beta)_x - tau(alpha)_x) + rc (tau(alpha)_y - tau(beta)_y) @@ -247,8 +247,8 @@ library GyroECLPMath { */ function calculateInvariantWithError( uint256[] memory balances, - IGyroECLPPool.Params memory params, - IGyroECLPPool.DerivedParams memory derived + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived ) internal pure returns (int256, int256) { (int256 x, int256 y) = (balances[0].toInt256(), balances[1].toInt256()); @@ -308,8 +308,8 @@ library GyroECLPMath { function calcAtAChi( int256 x, int256 y, - IGyroECLPPool.Params memory p, - IGyroECLPPool.DerivedParams memory d + IGyroECLPPool.EclpParams memory p, + IGyroECLPPool.DerivedEclpParams memory d ) internal pure returns (int256 val) { // to save gas, pre-compute dSq^2 as it will be used 3 times int256 dSq2 = d.dSq.mulXpU(d.dSq); @@ -335,8 +335,8 @@ library GyroECLPMath { * only divide by this later, we will not need to worry about overflow in that operation if done in the right way. */ function calcAChiAChiInXp( - IGyroECLPPool.Params memory p, - IGyroECLPPool.DerivedParams memory d + IGyroECLPPool.EclpParams memory p, + IGyroECLPPool.DerivedEclpParams memory d ) internal pure returns (int256 val) { // To save gas, pre-compute dSq^3 as it will be used 4 times. int256 dSq3 = d.dSq.mulXpU(d.dSq).mulXpU(d.dSq); @@ -364,8 +364,8 @@ library GyroECLPMath { function calcMinAtxAChiySqPlusAtxSq( int256 x, int256 y, - IGyroECLPPool.Params memory p, - IGyroECLPPool.DerivedParams memory d + IGyroECLPPool.EclpParams memory p, + IGyroECLPPool.DerivedEclpParams memory d ) internal pure returns (int256 val) { //////////////////////////////////////////////////////////////////////////////////// // (At)_x^2 (A chi)_y^2 = (x^2 c^2 - xy2sc + y^2 s^2) (u^2 + 2uv/lambda + v^2/lambda^2) @@ -399,8 +399,8 @@ library GyroECLPMath { function calc2AtxAtyAChixAChiy( int256 x, int256 y, - IGyroECLPPool.Params memory p, - IGyroECLPPool.DerivedParams memory d + IGyroECLPPool.EclpParams memory p, + IGyroECLPPool.DerivedEclpParams memory d ) internal pure returns (int256 val) { //////////////////////////////////////////////////////////////////////////////////// // = ((x^2 - y^2)sc + yx(c^2-s^2)) * 2 * (zu + (wu + zv)/lambda + wv/lambda^2) @@ -421,8 +421,8 @@ library GyroECLPMath { function calcMinAtyAChixSqPlusAtySq( int256 x, int256 y, - IGyroECLPPool.Params memory p, - IGyroECLPPool.DerivedParams memory d + IGyroECLPPool.EclpParams memory p, + IGyroECLPPool.DerivedEclpParams memory d ) internal pure returns (int256 val) { //////////////////////////////////////////////////////////////////////////////////// // (At)_y^2 (A chi)_x^2 = (x^2 s^2 + xy2sc + y^2 c^2) * (z^2 + 2zw/lambda + w^2/lambda^2) @@ -451,8 +451,8 @@ library GyroECLPMath { function calcInvariantSqrt( int256 x, int256 y, - IGyroECLPPool.Params memory p, - IGyroECLPPool.DerivedParams memory d + IGyroECLPPool.EclpParams memory p, + IGyroECLPPool.DerivedEclpParams memory d ) internal pure returns (int256 val, int256 err) { val = calcMinAtxAChiySqPlusAtxSq(x, y, p, d) + calc2AtxAtyAChixAChiy(x, y, p, d); val = val + calcMinAtyAChixSqPlusAtySq(x, y, p, d); @@ -474,8 +474,8 @@ library GyroECLPMath { */ function calcSpotPrice0in1( uint256[] memory balances, - IGyroECLPPool.Params memory params, - IGyroECLPPool.DerivedParams memory derived, + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived, int256 invariant ) internal pure returns (uint256 px) { // Shift by virtual offsets to get v(t). @@ -509,8 +509,8 @@ library GyroECLPMath { * (0 = X, 1 = Y) */ function checkAssetBounds( - IGyroECLPPool.Params memory params, - IGyroECLPPool.DerivedParams memory derived, + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived, IGyroECLPPool.Vector2 memory invariant, int256 newBal, uint8 assetIndex @@ -530,13 +530,16 @@ library GyroECLPMath { uint256[] memory balances, uint256 amountIn, bool tokenInIsToken0, - IGyroECLPPool.Params memory params, - IGyroECLPPool.DerivedParams memory derived, + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived, IGyroECLPPool.Vector2 memory invariant ) internal pure returns (uint256 amountOut) { - function(int256, IGyroECLPPool.Params memory, IGyroECLPPool.DerivedParams memory, IGyroECLPPool.Vector2 memory) - pure - returns (int256) calcGiven; + function( + int256, + IGyroECLPPool.EclpParams memory, + IGyroECLPPool.DerivedEclpParams memory, + IGyroECLPPool.Vector2 memory + ) pure returns (int256) calcGiven; uint8 ixIn; uint8 ixOut; if (tokenInIsToken0) { @@ -561,13 +564,16 @@ library GyroECLPMath { uint256[] memory balances, uint256 amountOut, bool tokenInIsToken0, - IGyroECLPPool.Params memory params, - IGyroECLPPool.DerivedParams memory derived, + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived, IGyroECLPPool.Vector2 memory invariant ) internal pure returns (uint256 amountIn) { - function(int256, IGyroECLPPool.Params memory, IGyroECLPPool.DerivedParams memory, IGyroECLPPool.Vector2 memory) - pure - returns (int256) calcGiven; + function( + int256, + IGyroECLPPool.EclpParams memory, + IGyroECLPPool.DerivedEclpParams memory, + IGyroECLPPool.Vector2 memory + ) pure returns (int256) calcGiven; uint8 ixIn; uint8 ixOut; if (tokenInIsToken0) { @@ -729,8 +735,8 @@ library GyroECLPMath { */ function calcYGivenX( int256 x, - IGyroECLPPool.Params memory params, - IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory d, IGyroECLPPool.Vector2 memory r // overestimate in x component, underestimate in y ) internal pure returns (int256 y) { // Want to overestimate the virtual offsets except in a particular setting that will be corrected for later. @@ -745,8 +751,8 @@ library GyroECLPMath { function calcXGivenY( int256 y, - IGyroECLPPool.Params memory params, - IGyroECLPPool.DerivedParams memory d, + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory d, IGyroECLPPool.Vector2 memory r // overestimate in x component, underestimate in y ) internal pure returns (int256 x) { // Want to overestimate the virtual offsets except in a particular setting that will be corrected for later. diff --git a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol index 34a97b650..60f5d9b88 100644 --- a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol +++ b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol @@ -61,7 +61,7 @@ contract GyroEclpPoolDeployer is BaseContractsDeployer { // Avoids Stack-too-deep. { - IGyroECLPPool.Params memory params = IGyroECLPPool.Params({ + IGyroECLPPool.EclpParams memory params = IGyroECLPPool.EclpParams({ alpha: _paramsAlpha, beta: _paramsBeta, c: _paramsC, @@ -69,7 +69,7 @@ contract GyroEclpPoolDeployer is BaseContractsDeployer { lambda: _paramsLambda }); - IGyroECLPPool.DerivedParams memory derivedParams = IGyroECLPPool.DerivedParams({ + IGyroECLPPool.DerivedEclpParams memory derivedParams = IGyroECLPPool.DerivedEclpParams({ tauAlpha: IGyroECLPPool.Vector2(_tauAlphaX, _tauAlphaY), tauBeta: IGyroECLPPool.Vector2(_tauBetaX, _tauBetaY), u: _u, From 34c453ec0aac582d154f0afb980d28fc7c89eb94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 30 Oct 2024 16:25:53 -0300 Subject: [PATCH 37/52] Improve documentation of the E-CLP pool --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 12 ++++++++++++ pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 99bd50b02..8dfcc462f 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -22,6 +22,12 @@ import { BalancerPoolToken } from "@balancer-labs/v3-vault/contracts/BalancerPoo import { GyroECLPMath } from "./lib/GyroECLPMath.sol"; +/** + * @notice Standard Gyro E-CLP Pool, with fixed E-CLP parameters. + * @dev Gyroscope's E-CLPs are AMMs where trading takes place along part of an ellipse curve. A given E-CLP is + * parameterized by the pricing range [α,β], the inclination angle `phi` and stretching parameter `lambda`. For more + * information, please refer to https://docs.gyro.finance/gyroscope-protocol/concentrated-liquidity-pools/e-clps. + */ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { using FixedPoint for uint256; using SafeCast for *; @@ -32,6 +38,11 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { int256 internal immutable _paramsC; int256 internal immutable _paramsS; int256 internal immutable _paramsLambda; + + /** + * @dev Derived Parameters of the E-CLP pool, calculated off-chain based on the parameters above. 38 decimals + * precision. + */ int256 internal immutable _tauAlphaX; int256 internal immutable _tauAlphaY; int256 internal immutable _tauBetaX; @@ -41,6 +52,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { int256 internal immutable _w; int256 internal immutable _z; int256 internal immutable _dSq; + bytes32 private constant _POOL_TYPE = "ECLP"; constructor(GyroECLPPoolParams memory params, IVault vault) BalancerPoolToken(vault, params.name, params.symbol) { diff --git a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol index 85e50e511..039473f80 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol @@ -14,8 +14,8 @@ import { BasePoolFactory } from "@balancer-labs/v3-pool-utils/contracts/BasePool import { GyroECLPPool } from "./GyroECLPPool.sol"; /** - * @notice Gyro ECLP Pool factory - * @dev This is the most general factory, which allows two tokens. + * @notice Gyro E-CLP Pool factory. + * @dev This is the pool factory for Gyro E-CLP pools, which supports two tokens only. */ contract GyroECLPPoolFactory is BasePoolFactory { // solhint-disable not-rely-on-time From 7dffc499dc1baca03d0b2853de8361bdf3434a13 Mon Sep 17 00:00:00 2001 From: Steffen Schuldenzucker Date: Wed, 27 Nov 2024 13:20:46 +0100 Subject: [PATCH 38/52] Fix comments in gyro eclp new port (#1140) --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 1 - pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 6 +++--- pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 8dfcc462f..d0268f9da 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -177,7 +177,6 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { invariant ); - // Fees are added after scaling happens, to reduce the complexity of the rounding direction analysis. return amountInScaled18; } } diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index 2ec0ab54c..b5931d0fc 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -496,7 +496,7 @@ library GyroECLPMath { IGyroECLPPool.Vector2 memory pc = IGyroECLPPool.Vector2(vec.x.divDownMagU(vec.y), _ONE); // Convert prices back to ellipse - // NB: These operations check for overflow because the price pc[0] might be large when vex.y is small. + // NB: These operations check for overflow because the price pc[0] might be large when vec.y is small. // SOMEDAY I think this probably can't actually happen due to our bounds on the different values. In this case // we could do this unchecked as well. int256 pgx = scalarProd(pc, mulA(params, IGyroECLPPool.Vector2(_ONE, 0))); @@ -596,7 +596,7 @@ library GyroECLPMath { } /** - * @dev Variables are named for calculating y given x. To calculate x given y, change x->y, s->c, c->s, a_>b, b->a, + * @dev Variables are named for calculating y given x. To calculate x given y, change x->y, s->c, c->s, a->b, b->a, * tauBeta.x -> -tauAlpha.x, tauBeta.y -> tauAlpha.y. Also, calculates an overestimate of calculated reserve * post-swap. */ @@ -637,7 +637,7 @@ library GyroECLPMath { sTerm.y = sTerm.y.mulUpMagU(s).divXpU(dSq + 1) + 1; // account for rounding error in dSq, divXp sTerm = IGyroECLPPool.Vector2(SignedFixedPoint.ONE_XP - sTerm.x, SignedFixedPoint.ONE_XP - sTerm.y); // ^^ NB: The components of sTerm are non-negative: We only need to worry about sTerm.y. This is non-negative - // because, because of bounds on lambda lamBar <= 1 - 1e-16, and division by dSq ensures we have enough + // because, because of bounds on lambda lamBar <= 1 - 1e-16 and division by dSq ensures we have enough // precision so that rounding errors are never magnitude 1e-16. // Now compute the argument of the square root. diff --git a/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol b/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol index 9680f56a0..f6f97073f 100644 --- a/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol +++ b/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol @@ -76,7 +76,7 @@ library SignedFixedPoint { } /** - * @dev this implements mulDownMag without checking for over/under-flows, which saves significantly on gas if these + * @dev this implements mulUpMag without checking for over/under-flows, which saves significantly on gas if these * aren't needed */ function mulUpMagU(int256 a, int256 b) internal pure returns (int256) { @@ -106,7 +106,7 @@ library SignedFixedPoint { } /** - * @dev this implements mulDownMag without checking for over/under-flows, which saves significantly on gas if these + * @dev this implements divDownMag without checking for over/under-flows, which saves significantly on gas if these * aren't needed */ function divDownMagU(int256 a, int256 b) internal pure returns (int256) { @@ -136,7 +136,7 @@ library SignedFixedPoint { } /** - * @dev this implements mulDownMag without checking for over/under-flows, which saves significantly on gas if these + * @dev this implements divUpMag without checking for over/under-flows, which saves significantly on gas if these * aren't needed */ function divUpMagU(int256 a, int256 b) internal pure returns (int256) { From ac793f3420486f055d4ee29c65cf20812f9fdb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 27 Nov 2024 15:17:22 -0300 Subject: [PATCH 39/52] Remove error factor when rounding invariant up --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 8 +- pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol | 40 ---------- pkg/pool-gyro/test/foundry/E2eSwap.t.sol | 79 ------------------- .../test/foundry/E2eSwapRateProvider.t.sol | 75 ------------------ .../foundry/LiquidityApproximationECLP.t.sol | 3 + .../foundry/LiquidityApproximationGyro.t.sol | 22 ------ .../foundry/utils/GyroEclpPoolDeployer.sol | 4 +- 7 files changed, 11 insertions(+), 220 deletions(-) delete mode 100644 pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol delete mode 100644 pkg/pool-gyro/test/foundry/E2eSwap.t.sol delete mode 100644 pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol delete mode 100644 pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index d0268f9da..ef508434e 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -99,7 +99,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { if (rounding == Rounding.ROUND_DOWN) { return currentInvariant.toUint256(); } else { - return (currentInvariant + 20 * invErr).toUint256(); + return (currentInvariant + invErr).toUint256(); } } @@ -200,12 +200,14 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { /// @inheritdoc ISwapFeePercentageBounds function getMinimumSwapFeePercentage() external pure returns (uint256) { - return 0; + // Liquidity Approximation tests shows that add/remove liquidity combinations are more profitable than a swap + // if the swap fee percentage is 0%, which is not desirable. So, a minimum percentage must be enforced. + return 1e12; // 0.000001% } /// @inheritdoc ISwapFeePercentageBounds function getMaximumSwapFeePercentage() external pure returns (uint256) { - return 1e18; + return 1e18; // 100% } /// @inheritdoc IUnbalancedLiquidityInvariantRatioBounds diff --git a/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol b/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol deleted file mode 100644 index e89eaf910..000000000 --- a/pkg/pool-gyro/test/foundry/E2eBatchSwap.t.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; - -import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; - -import { ERC20TestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/ERC20TestToken.sol"; - -import { E2eBatchSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eBatchSwap.t.sol"; - -import { Gyro2ClpPoolDeployer } from "./utils/Gyro2ClpPoolDeployer.sol"; - -contract E2eBatchSwapGyro2CLPTest is E2eBatchSwapTest, Gyro2ClpPoolDeployer { - /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eBatchSwapTest tests. - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { - IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); - return createGyro2ClpPool(tokens, rateProviders, label, vault, lp); - } - - function _setUpVariables() internal override { - tokenA = dai; - tokenB = usdc; - tokenC = ERC20TestToken(address(weth)); - tokenD = wsteth; - sender = lp; - poolCreator = lp; - - // If there are swap fees, the amountCalculated may be lower than MIN_TRADE_AMOUNT. So, multiplying - // MIN_TRADE_AMOUNT by 10 creates a margin. - minSwapAmountTokenA = 10 * PRODUCTION_MIN_TRADE_AMOUNT; - minSwapAmountTokenD = 10 * PRODUCTION_MIN_TRADE_AMOUNT; - - // Divide init amount by 10 to make sure weighted math ratios are respected (Cannot trade more than 30% of pool - // balance). - maxSwapAmountTokenA = poolInitAmount / 10; - maxSwapAmountTokenD = poolInitAmount / 10; - } -} diff --git a/pkg/pool-gyro/test/foundry/E2eSwap.t.sol b/pkg/pool-gyro/test/foundry/E2eSwap.t.sol deleted file mode 100644 index 35b3c03a2..000000000 --- a/pkg/pool-gyro/test/foundry/E2eSwap.t.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; - -import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; - -import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; - -import { E2eSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwap.t.sol"; - -import { Gyro2ClpPoolDeployer } from "./utils/Gyro2ClpPoolDeployer.sol"; - -contract E2eSwapGyro2CLPTest is E2eSwapTest, Gyro2ClpPoolDeployer { - using FixedPoint for uint256; - - function setUp() public override { - E2eSwapTest.setUp(); - } - - /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eSwapTest tests. - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { - IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); - return createGyro2ClpPool(tokens, rateProviders, label, vault, lp); - } - - function setUpVariables() internal override { - sender = lp; - poolCreator = lp; - - // 0.0001% min swap fee. - minPoolSwapFeePercentage = 1e12; - // 10% max swap fee. - maxPoolSwapFeePercentage = 10e16; - } - - function calculateMinAndMaxSwapAmounts() internal virtual override { - uint256 rateTokenA = getRate(tokenA); - uint256 rateTokenB = getRate(tokenB); - - // The vault does not allow trade amounts (amountGivenScaled18 or amountCalculatedScaled18) to be less than - // MIN_TRADE_AMOUNT. For "linear pools" (PoolMock), amountGivenScaled18 and amountCalculatedScaled18 are - // the same. So, minAmountGivenScaled18 > MIN_TRADE_AMOUNT. To derive the formula below, note that - // `amountGivenRaw = amountGivenScaled18/(rateToken * scalingFactor)`. There's an adjustment for stable math - // in the following steps. - uint256 tokenAMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenA).mulUp(10 ** decimalsTokenA); - uint256 tokenBMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenB).mulUp(10 ** decimalsTokenB); - - // Also, since we undo the operation (reverse swap with the output of the first swap), amountCalculatedRaw - // cannot be 0. Considering that amountCalculated is tokenB, and amountGiven is tokenA: - // 1) amountCalculatedRaw > 0 - // 2) amountCalculatedRaw = amountCalculatedScaled18 * 10^(decimalsB) / (rateB * 10^18) - // 3) amountCalculatedScaled18 = amountGivenScaled18 // Linear math, there's a factor to stable math - // 4) amountGivenScaled18 = amountGivenRaw * rateA * 10^18 / 10^(decimalsA) - // Using the four formulas above, we determine that: - // amountCalculatedRaw > rateB * 10^(decimalsA) / (rateA * 10^(decimalsB)) - uint256 tokenACalculatedNotZero = (rateTokenB * (10 ** decimalsTokenA)) / (rateTokenA * (10 ** decimalsTokenB)); - uint256 tokenBCalculatedNotZero = (rateTokenA * (10 ** decimalsTokenB)) / (rateTokenB * (10 ** decimalsTokenA)); - - // Use the larger of the two values above to calculate the minSwapAmount. Also, multiply by 10 to account for - // swap fees and compensate for rate rounding issues. - uint256 mathFactor = 10; - minSwapAmountTokenA = ( - tokenAMinTradeAmount > tokenACalculatedNotZero - ? mathFactor * tokenAMinTradeAmount - : mathFactor * tokenACalculatedNotZero - ); - minSwapAmountTokenB = ( - tokenBMinTradeAmount > tokenBCalculatedNotZero - ? mathFactor * tokenBMinTradeAmount - : mathFactor * tokenBCalculatedNotZero - ); - - // 50% of pool init amount to make sure LP has enough tokens to pay for the swap in case of EXACT_OUT. - maxSwapAmountTokenA = poolInitAmountTokenA.mulDown(50e16); - maxSwapAmountTokenB = poolInitAmountTokenB.mulDown(50e16); - } -} diff --git a/pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol deleted file mode 100644 index c31253414..000000000 --- a/pkg/pool-gyro/test/foundry/E2eSwapRateProvider.t.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; - -import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; - -import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; - -import { RateProviderMock } from "@balancer-labs/v3-vault/contracts/test/RateProviderMock.sol"; -import { E2eSwapRateProviderTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwapRateProvider.t.sol"; -import { VaultContractsDeployer } from "@balancer-labs/v3-vault/test/foundry/utils/VaultContractsDeployer.sol"; - -import { Gyro2ClpPoolDeployer } from "./utils/Gyro2ClpPoolDeployer.sol"; - -contract E2eSwapRateProviderGyro2CLPTest is VaultContractsDeployer, E2eSwapRateProviderTest, Gyro2ClpPoolDeployer { - using FixedPoint for uint256; - - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { - rateProviderTokenA = deployRateProviderMock(); - rateProviderTokenB = deployRateProviderMock(); - // Mock rates, so all tests that keep the rate constant use a rate different than 1. - rateProviderTokenA.mockRate(5.2453235e18); - rateProviderTokenB.mockRate(0.4362784e18); - - IRateProvider[] memory rateProviders = new IRateProvider[](2); - rateProviders[tokenAIdx] = IRateProvider(address(rateProviderTokenA)); - rateProviders[tokenBIdx] = IRateProvider(address(rateProviderTokenB)); - - return createGyro2ClpPool(tokens, rateProviders, label, vault, lp); - } - - function calculateMinAndMaxSwapAmounts() internal virtual override { - uint256 rateTokenA = getRate(tokenA); - uint256 rateTokenB = getRate(tokenB); - - // The vault does not allow trade amounts (amountGivenScaled18 or amountCalculatedScaled18) to be less than - // PRODUCTION_MIN_TRADE_AMOUNT. For "linear pools" (PoolMock), amountGivenScaled18 and amountCalculatedScaled18 - // are the same. So, minAmountGivenScaled18 > PRODUCTION_MIN_TRADE_AMOUNT. To derive the formula below, note - // that `amountGivenRaw = amountGivenScaled18/(rateToken * scalingFactor)`. There's an adjustment for stable - // math in the following steps. - uint256 tokenAMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenA).mulUp(10 ** decimalsTokenA); - uint256 tokenBMinTradeAmount = PRODUCTION_MIN_TRADE_AMOUNT.divUp(rateTokenB).mulUp(10 ** decimalsTokenB); - - // Also, since we undo the operation (reverse swap with the output of the first swap), amountCalculatedRaw - // cannot be 0. Considering that amountCalculated is tokenB, and amountGiven is tokenA: - // 1) amountCalculatedRaw > 0 - // 2) amountCalculatedRaw = amountCalculatedScaled18 * 10^(decimalsB) / (rateB * 10^18) - // 3) amountCalculatedScaled18 = amountGivenScaled18 // Linear math, there's a factor to stable math - // 4) amountGivenScaled18 = amountGivenRaw * rateA * 10^18 / 10^(decimalsA) - // Combining the four formulas above, we determine that: - // amountCalculatedRaw > rateB * 10^(decimalsA) / (rateA * 10^(decimalsB)) - uint256 tokenACalculatedNotZero = (rateTokenB * (10 ** decimalsTokenA)) / (rateTokenA * (10 ** decimalsTokenB)); - uint256 tokenBCalculatedNotZero = (rateTokenA * (10 ** decimalsTokenB)) / (rateTokenB * (10 ** decimalsTokenA)); - - // Use the larger of the two values above to calculate the minSwapAmount. Also, multiply by 100 to account for - // swap fees and compensate for rate and math rounding issues. - uint256 mathFactor = 100; - minSwapAmountTokenA = ( - tokenAMinTradeAmount > tokenACalculatedNotZero - ? mathFactor * tokenAMinTradeAmount - : mathFactor * tokenACalculatedNotZero - ); - minSwapAmountTokenB = ( - tokenBMinTradeAmount > tokenBCalculatedNotZero - ? mathFactor * tokenBMinTradeAmount - : mathFactor * tokenBCalculatedNotZero - ); - - // 50% of pool init amount to make sure LP has enough tokens to pay for the swap in case of EXACT_OUT. - maxSwapAmountTokenA = poolInitAmountTokenA.mulDown(50e16); - maxSwapAmountTokenB = poolInitAmountTokenB.mulDown(50e16); - } -} diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol index e10058e8e..71334c1b1 100644 --- a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; +import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; import { LiquidityApproximationTest } from "@balancer-labs/v3-vault/test/foundry/LiquidityApproximation.t.sol"; @@ -14,6 +15,8 @@ contract LiquidityApproximationECLPTest is LiquidityApproximationTest, GyroEclpP function setUp() public virtual override { LiquidityApproximationTest.setUp(); + minSwapFeePercentage = IBasePool(swapPool).getMinimumSwapFeePercentage(); + // The invariant of ECLP pools are smaller. maxAmount = 1e6 * 1e18; } diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol deleted file mode 100644 index 437ade812..000000000 --- a/pkg/pool-gyro/test/foundry/LiquidityApproximationGyro.t.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; - -import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; - -import { LiquidityApproximationTest } from "@balancer-labs/v3-vault/test/foundry/LiquidityApproximation.t.sol"; - -import { Gyro2ClpPoolDeployer } from "./utils/Gyro2ClpPoolDeployer.sol"; - -contract LiquidityApproximationGyroTest is LiquidityApproximationTest, Gyro2ClpPoolDeployer { - function setUp() public virtual override { - LiquidityApproximationTest.setUp(); - } - - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { - IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); - return createGyro2ClpPool(tokens, rateProviders, label, vault, lp); - } -} diff --git a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol index 60f5d9b88..7a385e638 100644 --- a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol +++ b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol @@ -21,6 +21,8 @@ import { GyroECLPPool } from "../../../contracts/GyroECLPPool.sol"; contract GyroEclpPoolDeployer is BaseContractsDeployer { using CastingHelpers for address[]; + uint256 internal MIN_SWAP_FEE_PERCENTAGE = 1e12; + // Extracted from pool 0x2191df821c198600499aa1f0031b1a7514d7a7d9 on Mainnet. int256 internal _paramsAlpha = 998502246630054917; int256 internal _paramsBeta = 1000200040008001600; @@ -89,7 +91,7 @@ contract GyroEclpPoolDeployer is BaseContractsDeployer { params, derivedParams, roleAccounts, - 0, + MIN_SWAP_FEE_PERCENTAGE, address(0), bytes32("") ) From 8741a2e932889e6b00b14df931770ebea0fed2e9 Mon Sep 17 00:00:00 2001 From: Steffen Schuldenzucker Date: Wed, 4 Dec 2024 01:21:29 +0100 Subject: [PATCH 40/52] Add check to GyroECLPPool.computeBalance() against MAX_INVARIANT (#1158) --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index ef508434e..c8b729073 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -124,6 +124,13 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { (currentInvariant + 2 * invErr).toUint256().mulUp(invariantRatio).toInt256(), currentInvariant.toUint256().mulUp(invariantRatio).toInt256() ); + + // Edge case check. Should never happen except for insane tokens. + // If this is hit, actually adding the tokens would lead to a revert or (if it + // went through) a deadlock downstream, so we catch it here. + if (invariant.y > GyroECLPMath._MAX_INVARIANT) { + revert GyroECLPMath.MaxInvariantExceeded(); + } } if (tokenInIndex == 0) { From 97f9e7257f60634605c3238c338ca1e09e1baf5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 6 Dec 2024 14:51:01 -0300 Subject: [PATCH 41/52] Fix ECLP tests --- .../test/foundry/E2eBatchSwapECLP.t.sol | 5 ++- pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol | 5 ++- .../foundry/E2eSwapRateProviderECLP.t.sol | 5 ++- .../test/foundry/FungibilityGyroECLP.t.sol | 5 ++- .../foundry/LiquidityApproximationECLP.t.sol | 5 ++- .../foundry/utils/GyroEclpPoolDeployer.sol | 45 +++++++++++-------- 6 files changed, 47 insertions(+), 23 deletions(-) diff --git a/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol index e738169de..00df5682a 100644 --- a/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol @@ -14,7 +14,10 @@ import { GyroEclpPoolDeployer } from "./utils/GyroEclpPoolDeployer.sol"; contract E2eBatchSwapECLPTest is E2eBatchSwapTest, GyroEclpPoolDeployer { /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eBatchSwapTest tests. - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + function _createPool( + address[] memory tokens, + string memory label + ) internal override returns (address, bytes memory) { IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); return createGyroEclpPool(tokens, rateProviders, label, vault, lp); } diff --git a/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol index c768afd05..9db0facd1 100644 --- a/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol @@ -20,7 +20,10 @@ contract E2eSwapECLPTest is E2eSwapTest, GyroEclpPoolDeployer { } /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eSwapTest tests. - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + function _createPool( + address[] memory tokens, + string memory label + ) internal override returns (address, bytes memory) { IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); return createGyroEclpPool(tokens, rateProviders, label, vault, lp); } diff --git a/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol index 02d6feb97..069bb1cd5 100644 --- a/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol @@ -18,7 +18,10 @@ contract E2eSwapRateProviderECLPTest is VaultContractsDeployer, E2eSwapRateProvi using FixedPoint for uint256; /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eSwapTest tests. - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + function _createPool( + address[] memory tokens, + string memory label + ) internal override returns (address, bytes memory) { rateProviderTokenA = deployRateProviderMock(); rateProviderTokenB = deployRateProviderMock(); // Mock rates, so all tests that keep the rate constant use a rate different than 1. diff --git a/pkg/pool-gyro/test/foundry/FungibilityGyroECLP.t.sol b/pkg/pool-gyro/test/foundry/FungibilityGyroECLP.t.sol index 5fef55882..b5d902914 100644 --- a/pkg/pool-gyro/test/foundry/FungibilityGyroECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/FungibilityGyroECLP.t.sol @@ -12,7 +12,10 @@ import { GyroEclpPoolDeployer } from "./utils/GyroEclpPoolDeployer.sol"; contract FungibilityGyroECLPTest is FungibilityTest, GyroEclpPoolDeployer { /// @notice Overrides BaseVaultTest _createPool(). This pool is used by FungibilityTest. - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + function _createPool( + address[] memory tokens, + string memory label + ) internal override returns (address, bytes memory) { IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); return createGyroEclpPool(tokens, rateProviders, label, vault, lp); } diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol index 71334c1b1..6b6ec454f 100644 --- a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol @@ -21,7 +21,10 @@ contract LiquidityApproximationECLPTest is LiquidityApproximationTest, GyroEclpP maxAmount = 1e6 * 1e18; } - function _createPool(address[] memory tokens, string memory label) internal override returns (address) { + function _createPool( + address[] memory tokens, + string memory label + ) internal override returns (address, bytes memory) { IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); return createGyroEclpPool(tokens, rateProviders, label, vault, lp); } diff --git a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol index 7a385e638..a5baed623 100644 --- a/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol +++ b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol @@ -55,11 +55,10 @@ contract GyroEclpPoolDeployer is BaseContractsDeployer { string memory label, IVaultMock vault, address poolCreator - ) internal returns (address) { + ) internal returns (address newPool, bytes memory poolArgs) { GyroECLPPoolFactory factory = deployGyroECLPPoolFactory(vault); PoolRoleAccounts memory roleAccounts; - GyroECLPPool newPool; // Avoids Stack-too-deep. { @@ -83,29 +82,39 @@ contract GyroEclpPoolDeployer is BaseContractsDeployer { TokenConfig[] memory tokenConfig = vault.buildTokenConfig(tokens.asIERC20(), rateProviders); - newPool = GyroECLPPool( - factory.create( - label, - label, - tokenConfig, - params, - derivedParams, - roleAccounts, - MIN_SWAP_FEE_PERCENTAGE, - address(0), - bytes32("") + newPool = address( + GyroECLPPool( + factory.create( + label, + label, + tokenConfig, + params, + derivedParams, + roleAccounts, + MIN_SWAP_FEE_PERCENTAGE, + address(0), + bytes32("") + ) ) ); + + poolArgs = abi.encode( + IGyroECLPPool.GyroECLPPoolParams({ + name: label, + symbol: label, + eclpParams: params, + derivedEclpParams: derivedParams + }), + vault + ); } - vm.label(address(newPool), label); + vm.label(newPool, label); // Cannot set the pool creator directly on a standard Balancer stable pool factory. - vault.manualSetPoolCreator(address(newPool), poolCreator); + vault.manualSetPoolCreator(newPool, poolCreator); ProtocolFeeControllerMock feeController = ProtocolFeeControllerMock(address(vault.getProtocolFeeController())); - feeController.manualSetPoolCreator(address(newPool), poolCreator); - - return address(newPool); + feeController.manualSetPoolCreator(newPool, poolCreator); } function deployGyroECLPPoolFactory(IVault vault) internal returns (GyroECLPPoolFactory) { From 364803b9f2eb94a6f260419edb1640bc2eeb71ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 12 Dec 2024 12:12:04 -0300 Subject: [PATCH 42/52] Fix PR comments --- pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol | 8 ++++++++ pkg/pool-gyro/contracts/GyroECLPPool.sol | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol b/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol index 7d3b03510..94a9c02aa 100644 --- a/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol +++ b/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol @@ -49,6 +49,14 @@ interface IGyroECLPPool is IBasePool { * and increase the precision. Therefore, the numbers are stored with 38 decimals precision. Please refer to * https://docs.gyro.finance/gyroscope-protocol/technical-documents, document "E-CLP high-precision * calculations.pdf", for further explanations on how to obtain the parameters below. + * + * @param tauAlpha + * @param tauBeta + * @param u from (A chi)_y = lambda * u + v + * @param v from (A chi)_y = lambda * u + v + * @param w from (A chi)_x = w / lambda + z + * @param z from (A chi)_x = w / lambda + z + * @param dSq error in c^2 + s^2 = dSq, used to correct errors in c, s, tau, u,v,w,z calculations */ struct DerivedEclpParams { Vector2 tauAlpha; diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index c8b729073..80125ed8b 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -32,6 +32,8 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { using FixedPoint for uint256; using SafeCast for *; + bytes32 private constant _POOL_TYPE = "ECLP"; + /// @dev Parameters of the ECLP pool int256 internal immutable _paramsAlpha; int256 internal immutable _paramsBeta; @@ -53,8 +55,6 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { int256 internal immutable _z; int256 internal immutable _dSq; - bytes32 private constant _POOL_TYPE = "ECLP"; - constructor(GyroECLPPoolParams memory params, IVault vault) BalancerPoolToken(vault, params.name, params.symbol) { GyroECLPMath.validateParams(params.eclpParams); emit ECLPParamsValidated(true); @@ -97,7 +97,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { ); if (rounding == Rounding.ROUND_DOWN) { - return currentInvariant.toUint256(); + return (currentInvariant - invErr).toUint256(); } else { return (currentInvariant + invErr).toUint256(); } From e638a4857a2d8141332e89a22b07c39831a7607e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 12 Dec 2024 12:28:08 -0300 Subject: [PATCH 43/52] Explaining round up and down of invariant better --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 80125ed8b..e37d565f8 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -119,16 +119,17 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { derivedECLPParams ); - // invariant = overestimate in x-component, underestimate in y-component. + // The invariant vector contains the rounded up and rounded down invariant. Both are needed when computing + // the new balance, because of E-CLP math approximations. Depending on tauAlpha and tauBeta values, we + // want to use the invariant rounded up or rounded down to make sure we're conservative in the output. invariant = Vector2( - (currentInvariant + 2 * invErr).toUint256().mulUp(invariantRatio).toInt256(), - currentInvariant.toUint256().mulUp(invariantRatio).toInt256() + (currentInvariant + invErr).toUint256().mulUp(invariantRatio).toInt256(), + (currentInvariant - invErr).toUint256().mulUp(invariantRatio).toInt256() ); - // Edge case check. Should never happen except for insane tokens. - // If this is hit, actually adding the tokens would lead to a revert or (if it - // went through) a deadlock downstream, so we catch it here. - if (invariant.y > GyroECLPMath._MAX_INVARIANT) { + // Edge case check. Should never happen except for insane tokens. If this is hit, actually adding the + // tokens would lead to a revert or (if it went through) a deadlock downstream, so we catch it here. + if (invariant.x > GyroECLPMath._MAX_INVARIANT) { revert GyroECLPMath.MaxInvariantExceeded(); } } From 9de5bd108e777f73217a131cb7d028ef78698b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 12 Dec 2024 12:30:19 -0300 Subject: [PATCH 44/52] Fix comment --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index e37d565f8..0633b74ce 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -120,8 +120,8 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { ); // The invariant vector contains the rounded up and rounded down invariant. Both are needed when computing - // the new balance, because of E-CLP math approximations. Depending on tauAlpha and tauBeta values, we - // want to use the invariant rounded up or rounded down to make sure we're conservative in the output. + // the virtual offsets. Depending on tauAlpha and tauBeta values, we want to use the invariant rounded up + // or rounded down to make sure we're conservative in the output. invariant = Vector2( (currentInvariant + invErr).toUint256().mulUp(invariantRatio).toInt256(), (currentInvariant - invErr).toUint256().mulUp(invariantRatio).toInt256() From a534c6dbf34e4a8bc3c13f5c5eb5bfb2417e41ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 12 Dec 2024 15:17:34 -0300 Subject: [PATCH 45/52] Use solc 0.8.27 to Gyro E-CLP --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 3 ++- pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol | 2 +- pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 2 +- pkg/pool-gyro/hardhat.config.ts | 2 ++ pvt/common/hardhat-base-config.ts | 15 +++++++++++++++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 0633b74ce..566760b8f 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -2,7 +2,7 @@ // for information on licensing please see the README in the GitHub repository // . -pragma solidity ^0.8.24; +pragma solidity ^0.8.27; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -149,6 +149,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { /// @inheritdoc IBasePool function onSwap(PoolSwapParams memory request) external view onlyVault returns (uint256) { + // The Vault already checks that index in != index out. bool tokenInIsToken0 = request.indexIn == 0; (EclpParams memory eclpParams, DerivedEclpParams memory derivedECLPParams) = _reconstructECLPParams(); diff --git a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol index 039473f80..f875bc4df 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.24; +pragma solidity ^0.8.27; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index b5931d0fc..775a4b6b5 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -2,7 +2,7 @@ // for information on licensing please see the README in the GitHub repository // . -pragma solidity ^0.8.24; +pragma solidity ^0.8.27; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; diff --git a/pkg/pool-gyro/hardhat.config.ts b/pkg/pool-gyro/hardhat.config.ts index dfd54996c..3bbb6a085 100644 --- a/pkg/pool-gyro/hardhat.config.ts +++ b/pkg/pool-gyro/hardhat.config.ts @@ -1,4 +1,5 @@ import { HardhatUserConfig } from 'hardhat/config'; +import { name } from './package.json'; import { hardhatBaseConfig } from '@balancer-labs/v3-common'; @@ -14,6 +15,7 @@ import { warnings } from '@balancer-labs/v3-common/hardhat-base-config'; const config: HardhatUserConfig = { solidity: { compilers: hardhatBaseConfig.compilers, + overrides: { ...hardhatBaseConfig.overrides(name) }, }, warnings, }; diff --git a/pvt/common/hardhat-base-config.ts b/pvt/common/hardhat-base-config.ts index 1181d76a3..9ad9ece0c 100644 --- a/pvt/common/hardhat-base-config.ts +++ b/pvt/common/hardhat-base-config.ts @@ -62,6 +62,21 @@ const contractSettings: ContractSettings = { runs: 500, viaIR, }, + '@balancer-labs/v3-pool-gyro/contracts/GyroECLPPool.sol': { + version: '0.8.27', + runs: 9999, + viaIR, + }, + '@balancer-labs/v3-pool-gyro/contracts/lib/GyroECLPMath.sol': { + version: '0.8.27', + runs: 9999, + viaIR, + }, + '@balancer-labs/v3-pool-gyro/contracts/GyroECLPPoolFactory.sol': { + version: '0.8.27', + runs: 9999, + viaIR, + }, '@balancer-labs/v3-vault/contracts/VaultExtension.sol': { version: '0.8.26', runs: 500, From 95461af23f79041fc3355c65858420196b034d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 12 Dec 2024 15:31:24 -0300 Subject: [PATCH 46/52] Use require instead of revert --- pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 79 ++++++++------------ 1 file changed, 32 insertions(+), 47 deletions(-) diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index 775a4b6b5..e1204eb19 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -58,24 +58,17 @@ library GyroECLPMath { /// @dev Enforces limits and approximate normalization of the rotation vector. function validateParams(IGyroECLPPool.EclpParams memory params) internal pure { - if (0 > params.s || params.s > _ONE) { - revert RotationVectorWrong(); - } - - if (0 > params.c || params.c > _ONE) { - revert RotationVectorWrong(); - } + require(params.s > 0 && params.s < _ONE, RotationVectorWrong()); + require(params.c > 0 && params.c < _ONE, RotationVectorWrong()); IGyroECLPPool.Vector2 memory sc = IGyroECLPPool.Vector2(params.s, params.c); int256 scnorm2 = scalarProd(sc, sc); // squared norm - if (_ONE - _ROTATION_VECTOR_NORM_ACCURACY > scnorm2 || scnorm2 > _ONE + _ROTATION_VECTOR_NORM_ACCURACY) { - revert RotationVectorNotNormalized(); - } - - if (params.lambda < 0 || params.lambda > _MAX_STRETCH_FACTOR) { - revert StretchingFactorWrong(); - } + require( + scnorm2 > _ONE - _ROTATION_VECTOR_NORM_ACCURACY && scnorm2 < _ONE + _ROTATION_VECTOR_NORM_ACCURACY, + RotationVectorNotNormalized() + ); + require(params.lambda > 0 && params.lambda < _MAX_STRETCH_FACTOR, StretchingFactorWrong()); } /** @@ -89,34 +82,32 @@ library GyroECLPMath { int256 norm2; norm2 = scalarProdXp(derived.tauAlpha, derived.tauAlpha); - if (_ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP > norm2 || norm2 > _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP) { - revert DerivedTauNotNormalized(); - } + require( + norm2 > _ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP && norm2 < _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP, + DerivedTauNotNormalized() + ); norm2 = scalarProdXp(derived.tauBeta, derived.tauBeta); - if (_ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP > norm2 || norm2 > _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP) { - revert DerivedTauNotNormalized(); - } - - if (derived.u > _ONE_XP) revert DerivedUvwzWrong(); - if (derived.v > _ONE_XP) revert DerivedUvwzWrong(); - if (derived.w > _ONE_XP) revert DerivedUvwzWrong(); - if (derived.z > _ONE_XP) revert DerivedUvwzWrong(); - - if ( - _ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP > derived.dSq || - derived.dSq > _ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP - ) { - revert DerivedDsqWrong(); - } + require( + norm2 > _ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP && norm2 < _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP, + DerivedTauNotNormalized() + ); + require(derived.u < _ONE_XP, DerivedUvwzWrong()); + require(derived.v < _ONE_XP, DerivedUvwzWrong()); + require(derived.w < _ONE_XP, DerivedUvwzWrong()); + require(derived.z < _ONE_XP, DerivedUvwzWrong()); + + require( + derived.dSq > _ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP && + derived.dSq < _ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP, + DerivedDsqWrong() + ); // NB No anti-overflow checks are required given the checks done above and in validateParams(). int256 mulDenominator = _ONE_XP.divXpU(calcAChiAChiInXp(params, derived) - _ONE_XP); - if (mulDenominator > _MAX_INV_INVARIANT_DENOMINATOR_XP) { - revert InvariantDenominatorWrong(); - } + require(mulDenominator < _MAX_INV_INVARIANT_DENOMINATOR_XP, InvariantDenominatorWrong()); } function scalarProd( @@ -252,9 +243,7 @@ library GyroECLPMath { ) internal pure returns (int256, int256) { (int256 x, int256 y) = (balances[0].toInt256(), balances[1].toInt256()); - if (x + y > _MAX_BALANCES) { - revert MaxAssetsExceeded(); - } + require(x + y < _MAX_BALANCES, MaxAssetsExceeded()); int256 atAChi = calcAtAChi(x, y, params, derived); (int256 sqrt, int256 err) = calcInvariantSqrt(x, y, params, derived); @@ -297,9 +286,7 @@ library GyroECLPMath { _ONE_XP + 1; - if (invariant + err > _MAX_INVARIANT) { - revert MaxInvariantExceeded(); - } + require(invariant + err < _MAX_INVARIANT, MaxInvariantExceeded()); return (invariant, err); } @@ -517,12 +504,10 @@ library GyroECLPMath { ) internal pure { if (assetIndex == 0) { int256 xPlus = maxBalances0(params, derived, invariant); - if (!(newBal <= _MAX_BALANCES && newBal <= xPlus)) revert AssetBoundsExceeded(); - return; - } - { + require(newBal <= _MAX_BALANCES && newBal <= xPlus, AssetBoundsExceeded()); + } else { int256 yPlus = maxBalances1(params, derived, invariant); - if (!(newBal <= _MAX_BALANCES && newBal <= yPlus)) revert AssetBoundsExceeded(); + require(newBal <= _MAX_BALANCES && newBal <= yPlus, AssetBoundsExceeded()); } } @@ -586,7 +571,7 @@ library GyroECLPMath { calcGiven = calcYGivenX; // this reverses compared to calcOutGivenIn } - if (!(amountOut <= balances[ixOut])) revert AssetBoundsExceeded(); + require(amountOut <= balances[ixOut], AssetBoundsExceeded()); int256 balOutNew = (balances[ixOut] - amountOut).toInt256(); int256 balInNew = calcGiven(balOutNew, params, derived, invariant); // The checks in the following two lines should really always succeed; we keep them as extra safety against From 2a295d29660825eb443789e1d6bb5defdf8ac6ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 12 Dec 2024 15:33:00 -0300 Subject: [PATCH 47/52] Replace revert by require --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 4 +--- pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol | 9 ++------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 566760b8f..8a75fa6ba 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -129,9 +129,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { // Edge case check. Should never happen except for insane tokens. If this is hit, actually adding the // tokens would lead to a revert or (if it went through) a deadlock downstream, so we catch it here. - if (invariant.x > GyroECLPMath._MAX_INVARIANT) { - revert GyroECLPMath.MaxInvariantExceeded(); - } + require(invariant.x < GyroECLPMath._MAX_INVARIANT, GyroECLPMath.MaxInvariantExceeded()); } if (tokenInIndex == 0) { diff --git a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol index f875bc4df..5041f44f5 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol @@ -53,13 +53,8 @@ contract GyroECLPPoolFactory is BasePoolFactory { address poolHooksContract, bytes32 salt ) external returns (address pool) { - if (tokens.length != 2) { - revert SupportsOnlyTwoTokens(); - } - - if (roleAccounts.poolCreator != address(0)) { - revert StandardPoolWithCreator(); - } + require(tokens.length == 2, SupportsOnlyTwoTokens()); + require(roleAccounts.poolCreator == address(0), StandardPoolWithCreator()); pool = _create( abi.encode( From 51ddbef464bdccc7c5fbfa7c2c376f0e07ca8adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 12 Dec 2024 15:51:22 -0300 Subject: [PATCH 48/52] Fix Min and Max invariant ratios --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 4 +-- pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 32 ++++++++++++------- .../test/foundry/E2eBatchSwapECLP.t.sol | 7 ++-- .../foundry/LiquidityApproximationECLP.t.sol | 2 +- .../test/foundry/E2eBatchSwap.t.sol | 7 ++-- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 8a75fa6ba..6f7d4f8a7 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -219,11 +219,11 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { /// @inheritdoc IUnbalancedLiquidityInvariantRatioBounds function getMinimumInvariantRatio() external pure returns (uint256) { - return 0; + return GyroECLPMath.MIN_INVARIANT_RATIO; } /// @inheritdoc IUnbalancedLiquidityInvariantRatioBounds function getMaximumInvariantRatio() external pure returns (uint256) { - return type(uint256).max; + return GyroECLPMath.MAX_INVARIANT_RATIO; } } diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index e1204eb19..ae9778473 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -24,12 +24,17 @@ library GyroECLPMath { using SafeCast for uint256; using SafeCast for int256; - error RotationVectorWrong(); + error RotationVectorSWrong(); + error RotationVectorCWrong(); error RotationVectorNotNormalized(); error AssetBoundsExceeded(); - error DerivedTauNotNormalized(); + error DerivedTauAlphaNotNormalized(); + error DerivedTauBetaNotNormalized(); error StretchingFactorWrong(); - error DerivedUvwzWrong(); + error DerivedUWrong(); + error DerivedVWrong(); + error DerivedWWrong(); + error DerivedZWrong(); error InvariantDenominatorWrong(); error MaxAssetsExceeded(); error MaxInvariantExceeded(); @@ -50,6 +55,11 @@ library GyroECLPMath { int256 internal constant _MAX_BALANCES = 1e34; // 1e16 in normal precision int256 internal constant _MAX_INVARIANT = 3e37; // 3e19 in normal precision + // Invariant growth limit: non-proportional add cannot cause the invariant to increase by more than this ratio. + uint256 public constant MIN_INVARIANT_RATIO = 60e16; // 60% + // Invariant shrink limit: non-proportional remove cannot cause the invariant to decrease by less than this ratio. + uint256 public constant MAX_INVARIANT_RATIO = 500e16; // 500% + struct QParams { int256 a; int256 b; @@ -58,8 +68,8 @@ library GyroECLPMath { /// @dev Enforces limits and approximate normalization of the rotation vector. function validateParams(IGyroECLPPool.EclpParams memory params) internal pure { - require(params.s > 0 && params.s < _ONE, RotationVectorWrong()); - require(params.c > 0 && params.c < _ONE, RotationVectorWrong()); + require(params.s > 0 && params.s < _ONE, RotationVectorSWrong()); + require(params.c > 0 && params.c < _ONE, RotationVectorCWrong()); IGyroECLPPool.Vector2 memory sc = IGyroECLPPool.Vector2(params.s, params.c); int256 scnorm2 = scalarProd(sc, sc); // squared norm @@ -84,19 +94,19 @@ library GyroECLPMath { require( norm2 > _ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP && norm2 < _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP, - DerivedTauNotNormalized() + DerivedTauAlphaNotNormalized() ); norm2 = scalarProdXp(derived.tauBeta, derived.tauBeta); require( norm2 > _ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP && norm2 < _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP, - DerivedTauNotNormalized() + DerivedTauBetaNotNormalized() ); - require(derived.u < _ONE_XP, DerivedUvwzWrong()); - require(derived.v < _ONE_XP, DerivedUvwzWrong()); - require(derived.w < _ONE_XP, DerivedUvwzWrong()); - require(derived.z < _ONE_XP, DerivedUvwzWrong()); + require(derived.u < _ONE_XP, DerivedUWrong()); + require(derived.v < _ONE_XP, DerivedVWrong()); + require(derived.w < _ONE_XP, DerivedWWrong()); + require(derived.z < _ONE_XP, DerivedZWrong()); require( derived.dSq > _ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP && diff --git a/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol index 00df5682a..ef96857df 100644 --- a/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol @@ -35,9 +35,8 @@ contract E2eBatchSwapECLPTest is E2eBatchSwapTest, GyroEclpPoolDeployer { minSwapAmountTokenA = 10 * PRODUCTION_MIN_TRADE_AMOUNT; minSwapAmountTokenD = 10 * PRODUCTION_MIN_TRADE_AMOUNT; - // Divide init amount by 10 to make sure weighted math ratios are respected (Cannot trade more than 30% of pool - // balance). - maxSwapAmountTokenA = poolInitAmount / 10; - maxSwapAmountTokenD = poolInitAmount / 10; + // 25% of pool init amount, so MIN and MAX invariant ratios are not violated. + maxSwapAmountTokenA = poolInitAmount / 4; + maxSwapAmountTokenD = poolInitAmount / 4; } } diff --git a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol index 6b6ec454f..c8eac604e 100644 --- a/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol +++ b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol @@ -18,7 +18,7 @@ contract LiquidityApproximationECLPTest is LiquidityApproximationTest, GyroEclpP minSwapFeePercentage = IBasePool(swapPool).getMinimumSwapFeePercentage(); // The invariant of ECLP pools are smaller. - maxAmount = 1e6 * 1e18; + maxAmount = 1e5 * 1e18; } function _createPool( diff --git a/pkg/pool-stable/test/foundry/E2eBatchSwap.t.sol b/pkg/pool-stable/test/foundry/E2eBatchSwap.t.sol index b40c18e2a..a70567325 100644 --- a/pkg/pool-stable/test/foundry/E2eBatchSwap.t.sol +++ b/pkg/pool-stable/test/foundry/E2eBatchSwap.t.sol @@ -36,10 +36,9 @@ contract E2eBatchSwapStableTest is E2eBatchSwapTest, StablePoolContractsDeployer minSwapAmountTokenA = 10 * PRODUCTION_MIN_TRADE_AMOUNT; minSwapAmountTokenD = 10 * PRODUCTION_MIN_TRADE_AMOUNT; - // Divide init amount by 10 to make sure weighted math ratios are respected (Cannot trade more than 30% of pool - // balance). - maxSwapAmountTokenA = poolInitAmount / 10; - maxSwapAmountTokenD = poolInitAmount / 10; + // 25% of pool init amount, so MIN and MAX invariant ratios are not violated. + maxSwapAmountTokenA = poolInitAmount / 4; + maxSwapAmountTokenD = poolInitAmount / 4; } /// @notice Overrides BaseVaultTest _createPool(). This pool is used by E2eBatchSwapTest tests. From bcd563296235f46f45b9a652397ba0dbc8cbb853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 13 Dec 2024 10:27:50 -0300 Subject: [PATCH 49/52] Fix foundry version --- pkg/pool-gyro/foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/pool-gyro/foundry.toml b/pkg/pool-gyro/foundry.toml index 8b4179b43..cf85ccaf9 100755 --- a/pkg/pool-gyro/foundry.toml +++ b/pkg/pool-gyro/foundry.toml @@ -23,7 +23,7 @@ remappings = [ ] optimizer = true optimizer_runs = 999 -solc_version = '0.8.26' +solc_version = '0.8.27' auto_detect_solc = false evm_version = 'cancun' ignored_error_codes = [2394, 5574, 3860] # Transient storage, code size From b804c8cbd5397bed309bb3e020a0aeb0e7bcc4a2 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Ubeira Date: Tue, 17 Dec 2024 12:10:43 -0300 Subject: [PATCH 50/52] Adjust requires to match original code. --- pkg/pool-gyro/contracts/GyroECLPPool.sol | 2 +- pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 30 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/pool-gyro/contracts/GyroECLPPool.sol b/pkg/pool-gyro/contracts/GyroECLPPool.sol index 6f7d4f8a7..470e4801a 100644 --- a/pkg/pool-gyro/contracts/GyroECLPPool.sol +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -129,7 +129,7 @@ contract GyroECLPPool is IGyroECLPPool, BalancerPoolToken { // Edge case check. Should never happen except for insane tokens. If this is hit, actually adding the // tokens would lead to a revert or (if it went through) a deadlock downstream, so we catch it here. - require(invariant.x < GyroECLPMath._MAX_INVARIANT, GyroECLPMath.MaxInvariantExceeded()); + require(invariant.x <= GyroECLPMath._MAX_INVARIANT, GyroECLPMath.MaxInvariantExceeded()); } if (tokenInIndex == 0) { diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index ae9778473..24ef659b5 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -68,17 +68,17 @@ library GyroECLPMath { /// @dev Enforces limits and approximate normalization of the rotation vector. function validateParams(IGyroECLPPool.EclpParams memory params) internal pure { - require(params.s > 0 && params.s < _ONE, RotationVectorSWrong()); - require(params.c > 0 && params.c < _ONE, RotationVectorCWrong()); + require(0 <= params.s && params.s <= _ONE, RotationVectorSWrong()); + require(0 <= params.c && params.c <= _ONE, RotationVectorCWrong()); IGyroECLPPool.Vector2 memory sc = IGyroECLPPool.Vector2(params.s, params.c); int256 scnorm2 = scalarProd(sc, sc); // squared norm require( - scnorm2 > _ONE - _ROTATION_VECTOR_NORM_ACCURACY && scnorm2 < _ONE + _ROTATION_VECTOR_NORM_ACCURACY, + _ONE - _ROTATION_VECTOR_NORM_ACCURACY <= scnorm2 && scnorm2 <= _ONE + _ROTATION_VECTOR_NORM_ACCURACY, RotationVectorNotNormalized() ); - require(params.lambda > 0 && params.lambda < _MAX_STRETCH_FACTOR, StretchingFactorWrong()); + require(0 <= params.lambda && params.lambda <= _MAX_STRETCH_FACTOR, StretchingFactorWrong()); } /** @@ -93,31 +93,31 @@ library GyroECLPMath { norm2 = scalarProdXp(derived.tauAlpha, derived.tauAlpha); require( - norm2 > _ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP && norm2 < _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP, + _ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP <= norm2 && norm2 <= _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP, DerivedTauAlphaNotNormalized() ); norm2 = scalarProdXp(derived.tauBeta, derived.tauBeta); require( - norm2 > _ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP && norm2 < _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP, + _ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP <= norm2 && norm2 <= _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP, DerivedTauBetaNotNormalized() ); - require(derived.u < _ONE_XP, DerivedUWrong()); - require(derived.v < _ONE_XP, DerivedVWrong()); - require(derived.w < _ONE_XP, DerivedWWrong()); - require(derived.z < _ONE_XP, DerivedZWrong()); + + require(derived.u <= _ONE_XP, DerivedUWrong()); + require(derived.v <= _ONE_XP, DerivedVWrong()); + require(derived.w <= _ONE_XP, DerivedWWrong()); + require(derived.z <= _ONE_XP, DerivedZWrong()); require( - derived.dSq > _ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP && - derived.dSq < _ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP, + _ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP <= derived.dSq && derived.dSq <= _ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP, DerivedDsqWrong() ); // NB No anti-overflow checks are required given the checks done above and in validateParams(). int256 mulDenominator = _ONE_XP.divXpU(calcAChiAChiInXp(params, derived) - _ONE_XP); - require(mulDenominator < _MAX_INV_INVARIANT_DENOMINATOR_XP, InvariantDenominatorWrong()); + require(mulDenominator <= _MAX_INV_INVARIANT_DENOMINATOR_XP, InvariantDenominatorWrong()); } function scalarProd( @@ -253,7 +253,7 @@ library GyroECLPMath { ) internal pure returns (int256, int256) { (int256 x, int256 y) = (balances[0].toInt256(), balances[1].toInt256()); - require(x + y < _MAX_BALANCES, MaxAssetsExceeded()); + require(x + y <= _MAX_BALANCES, MaxAssetsExceeded()); int256 atAChi = calcAtAChi(x, y, params, derived); (int256 sqrt, int256 err) = calcInvariantSqrt(x, y, params, derived); @@ -296,7 +296,7 @@ library GyroECLPMath { _ONE_XP + 1; - require(invariant + err < _MAX_INVARIANT, MaxInvariantExceeded()); + require(invariant + err <= _MAX_INVARIANT, MaxInvariantExceeded()); return (invariant, err); } From 9c1a8ccf965e40f747d1dfd3cb8746cc3e171861 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Ubeira Date: Tue, 17 Dec 2024 12:34:54 -0300 Subject: [PATCH 51/52] Lint. --- .prettierrc.json | 8 ++++++++ pkg/pool-gyro/contracts/lib/GyroECLPMath.sol | 3 ++- pvt/common/hardhat-base-config.ts | 12 +----------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index c13b5afa0..452c71485 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -15,6 +15,14 @@ "compiler": "0.8.24" } }, + { + "files": "**/pool-gyro/**/*.sol", + "options": { + "singleQuote": false, + "tabWidth": 4, + "compiler": "0.8.27" + } + }, { "files": "*.md", "options": { diff --git a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol index 24ef659b5..2d51bc182 100644 --- a/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -110,7 +110,8 @@ library GyroECLPMath { require(derived.z <= _ONE_XP, DerivedZWrong()); require( - _ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP <= derived.dSq && derived.dSq <= _ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP, + _ONE_XP - _DERIVED_DSQ_NORM_ACCURACY_XP <= derived.dSq && + derived.dSq <= _ONE_XP + _DERIVED_DSQ_NORM_ACCURACY_XP, DerivedDsqWrong() ); diff --git a/pvt/common/hardhat-base-config.ts b/pvt/common/hardhat-base-config.ts index 9ad9ece0c..052812de7 100644 --- a/pvt/common/hardhat-base-config.ts +++ b/pvt/common/hardhat-base-config.ts @@ -62,17 +62,7 @@ const contractSettings: ContractSettings = { runs: 500, viaIR, }, - '@balancer-labs/v3-pool-gyro/contracts/GyroECLPPool.sol': { - version: '0.8.27', - runs: 9999, - viaIR, - }, - '@balancer-labs/v3-pool-gyro/contracts/lib/GyroECLPMath.sol': { - version: '0.8.27', - runs: 9999, - viaIR, - }, - '@balancer-labs/v3-pool-gyro/contracts/GyroECLPPoolFactory.sol': { + '@balancer-labs/v3-pool-gyro/contracts': { version: '0.8.27', runs: 9999, viaIR, From 773db2e6a6be22e2f58219618658e8da0afa331b Mon Sep 17 00:00:00 2001 From: Juan Ignacio Ubeira Date: Tue, 17 Dec 2024 16:27:39 -0300 Subject: [PATCH 52/52] Fix hardhat config. --- pvt/common/hardhat-base-config.ts | 47 +++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/pvt/common/hardhat-base-config.ts b/pvt/common/hardhat-base-config.ts index 052812de7..dd2ea9df7 100644 --- a/pvt/common/hardhat-base-config.ts +++ b/pvt/common/hardhat-base-config.ts @@ -23,7 +23,7 @@ const viaIR = !(process.env.COVERAGE === 'true' ? true : false); const optimizerSteps = 'dhfoDgvulfnTUtnIf [ xa[r]EscLM cCTUtTOntnfDIul Lcul Vcul [j] Tpeul xa[rul] xa[r]cL gvif CTUca[r]LSsTFOtfDnca[r]Iulc ] jmul[jul] VcTOcul jmul : fDnTOcmu'; -export const compilers: [SolcConfig] = [ +export const compilers: SolcConfig[] = [ { version: '0.8.26', settings: { @@ -40,6 +40,22 @@ export const compilers: [SolcConfig] = [ }, }, }, + { + version: '0.8.27', + settings: { + viaIR, + evmVersion: 'cancun', + optimizer: { + enabled: true, + runs: 9999, + details: { + yulDetails: { + optimizerSteps, + }, + }, + }, + }, + }, ]; type ContractSettings = Record< @@ -51,30 +67,37 @@ type ContractSettings = Record< } >; +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +const COMPILER_0_8_26 = compilers.find((compiler) => compiler.version === '0.8.26')!; +const COMPILER_0_8_27 = compilers.find((compiler) => compiler.version === '0.8.27')!; + +/* eslint-enable @typescript-eslint/no-non-null-assertion */ + const contractSettings: ContractSettings = { '@balancer-labs/v3-vault/contracts': { - version: compilers[0].version, - runs: compilers[0].settings.optimizer.runs, + version: COMPILER_0_8_26.version, + runs: COMPILER_0_8_26.settings.optimizer.runs, viaIR, }, '@balancer-labs/v3-vault/contracts/Vault.sol': { - version: '0.8.26', + version: COMPILER_0_8_26.version, runs: 500, viaIR, }, - '@balancer-labs/v3-pool-gyro/contracts': { - version: '0.8.27', - runs: 9999, - viaIR, - }, '@balancer-labs/v3-vault/contracts/VaultExtension.sol': { - version: '0.8.26', + version: COMPILER_0_8_26.version, runs: 500, viaIR, }, '@balancer-labs/v3-vault/contracts/VaultExplorer.sol': { - version: '0.8.24', - runs: 9999, + version: COMPILER_0_8_27.version, + runs: COMPILER_0_8_27.settings.optimizer.runs, + viaIR, + }, + '@balancer-labs/v3-pool-gyro/contracts': { + version: COMPILER_0_8_27.version, + runs: COMPILER_0_8_27.settings.optimizer.runs, viaIR, }, };