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/interfaces/contracts/pool-gyro/IGyroECLPPool.sol b/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol new file mode 100644 index 000000000..94a9c02aa --- /dev/null +++ b/pkg/interfaces/contracts/pool-gyro/IGyroECLPPool.sol @@ -0,0 +1,76 @@ +// 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 ECLP pool configuration. + * @param name Pool name + * @param symbol Pool symbol + * @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; + EclpParams eclpParams; + DerivedEclpParams derivedEclpParams; + } + + /** + * @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; + 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 + int256 lambda; + } + + /** + * @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. + * + * @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; + Vector2 tauBeta; + 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/Gyro2CLPPool.sol b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol index 2107fea70..a7353fdcf 100644 --- a/pkg/pool-gyro/contracts/Gyro2CLPPool.sol +++ b/pkg/pool-gyro/contracts/Gyro2CLPPool.sol @@ -40,7 +40,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); @@ -102,7 +105,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 new file mode 100644 index 000000000..470e4801a --- /dev/null +++ b/pkg/pool-gyro/contracts/GyroECLPPool.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: LicenseRef-Gyro-1.0 +// for information on licensing please see the README in the GitHub repository +// . + +pragma solidity ^0.8.27; + +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 { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.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 { 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 *; + + bytes32 private constant _POOL_TYPE = "ECLP"; + + /// @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; + + /** + * @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; + int256 internal immutable _tauBetaY; + int256 internal immutable _u; + int256 internal immutable _v; + int256 internal immutable _w; + int256 internal immutable _z; + int256 internal immutable _dSq; + + constructor(GyroECLPPoolParams 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 rounding + ) external view returns (uint256) { + (EclpParams memory eclpParams, DerivedEclpParams memory derivedECLPParams) = _reconstructECLPParams(); + + (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( + balancesLiveScaled18, + eclpParams, + derivedECLPParams + ); + + if (rounding == Rounding.ROUND_DOWN) { + return (currentInvariant - invErr).toUint256(); + } else { + return (currentInvariant + invErr).toUint256(); + } + } + + /// @inheritdoc IBasePool + function computeBalance( + uint256[] memory balancesLiveScaled18, + uint256 tokenInIndex, + uint256 invariantRatio + ) external view returns (uint256 newBalance) { + (EclpParams memory eclpParams, DerivedEclpParams memory derivedECLPParams) = _reconstructECLPParams(); + + Vector2 memory invariant; + { + (int256 currentInvariant, int256 invErr) = GyroECLPMath.calculateInvariantWithError( + balancesLiveScaled18, + eclpParams, + derivedECLPParams + ); + + // The invariant vector contains the rounded up and rounded down invariant. Both are needed when computing + // 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() + ); + + // 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()); + } + + 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 + 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(); + 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 = 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 + ); + + return amountInScaled18; + } + } + + /** @dev reconstructs ECLP params structs from immutable arrays */ + function _reconstructECLPParams() private view returns (EclpParams memory params, DerivedEclpParams 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 (EclpParams memory params, DerivedEclpParams memory d) { + return _reconstructECLPParams(); + } + + /// @inheritdoc ISwapFeePercentageBounds + function getMinimumSwapFeePercentage() external pure returns (uint256) { + // 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; // 100% + } + + /// @inheritdoc IUnbalancedLiquidityInvariantRatioBounds + function getMinimumInvariantRatio() external pure returns (uint256) { + return GyroECLPMath.MIN_INVARIANT_RATIO; + } + + /// @inheritdoc IUnbalancedLiquidityInvariantRatioBounds + function getMaximumInvariantRatio() external pure returns (uint256) { + return GyroECLPMath.MAX_INVARIANT_RATIO; + } +} diff --git a/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol b/pkg/pool-gyro/contracts/GyroECLPPoolFactory.sol new file mode 100644 index 000000000..5041f44f5 --- /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.27; + +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"; + +import { BasePoolFactory } from "@balancer-labs/v3-pool-utils/contracts/BasePoolFactory.sol"; + +import { GyroECLPPool } from "./GyroECLPPool.sol"; + +/** + * @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 + + /// @notice ECLP pools support 2 tokens only. + 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, + IGyroECLPPool.EclpParams memory eclpParams, + IGyroECLPPool.DerivedEclpParams memory derivedEclpParams, + PoolRoleAccounts memory roleAccounts, + uint256 swapFeePercentage, + address poolHooksContract, + bytes32 salt + ) external returns (address pool) { + require(tokens.length == 2, SupportsOnlyTwoTokens()); + require(roleAccounts.poolCreator == address(0), StandardPoolWithCreator()); + + pool = _create( + abi.encode( + IGyroECLPPool.GyroECLPPoolParams({ + 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..2d51bc182 --- /dev/null +++ b/pkg/pool-gyro/contracts/lib/GyroECLPMath.sol @@ -0,0 +1,773 @@ +// SPDX-License-Identifier: LicenseRef-Gyro-1.0 +// for information on licensing please see the README in the GitHub repository +// . + +pragma solidity ^0.8.27; + +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 } from "./SignedFixedPoint.sol"; +import { GyroPoolMath } from "./GyroPoolMath.sol"; + +/** + * @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 { + using SignedFixedPoint for int256; + using FixedPoint for uint256; + using SafeCast for uint256; + using SafeCast for int256; + + error RotationVectorSWrong(); + error RotationVectorCWrong(); + error RotationVectorNotNormalized(); + error AssetBoundsExceeded(); + error DerivedTauAlphaNotNormalized(); + error DerivedTauBetaNotNormalized(); + error StretchingFactorWrong(); + error DerivedUWrong(); + error DerivedVWrong(); + error DerivedWWrong(); + error DerivedZWrong(); + 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 + + // 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 + + // 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; + int256 c; + } + + /// @dev Enforces limits and approximate normalization of the rotation vector. + function validateParams(IGyroECLPPool.EclpParams memory params) internal pure { + 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( + _ONE - _ROTATION_VECTOR_NORM_ACCURACY <= scnorm2 && scnorm2 <= _ONE + _ROTATION_VECTOR_NORM_ACCURACY, + RotationVectorNotNormalized() + ); + require(0 <= params.lambda && params.lambda <= _MAX_STRETCH_FACTOR, StretchingFactorWrong()); + } + + /** + * @notice Enforces limits and approximate normalization of the derived values. + * @dev Does NOT check for internal consistency of 'derived' with 'params'. + */ + function validateDerivedParamsLimits( + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived + ) internal pure { + int256 norm2; + norm2 = scalarProdXp(derived.tauAlpha, derived.tauAlpha); + + require( + _ONE_XP - _DERIVED_TAU_NORM_ACCURACY_XP <= norm2 && norm2 <= _ONE_XP + _DERIVED_TAU_NORM_ACCURACY_XP, + DerivedTauAlphaNotNormalized() + ); + + norm2 = scalarProdXp(derived.tauBeta, derived.tauBeta); + + require( + _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( + _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()); + } + + 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( + IGyroECLPPool.Vector2 memory t1, + IGyroECLPPool.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. + + /** + * @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( + 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: + // 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); + } + + /** + * @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( + 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 + // 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)); + } + + /** + * @notice calculate virtual offset b given invariant r. + * @dev Calculates b = r*(A^{-1}tau(alpha))_y rounding up in signed direction + */ + function virtualOffset1( + 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 + // 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)); + } + + /** + * @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( + 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) + // account for 1 factors of dSq (2 s,c factors) + + // 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); + } + + /** + * @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( + 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) + // 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); + } + + /** + * @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, + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived + ) internal pure returns (int256, int256) { + (int256 x, int256 y) = (balances[0].toInt256(), balances[1].toInt256()); + + require(x + y <= _MAX_BALANCES, 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 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) / + _ONE_XP + + 1; + + require(invariant + err <= _MAX_INVARIANT, MaxInvariantExceeded()); + + return (invariant, err); + } + + /// @dev Calculate At \cdot A chi, ignores rounding direction. We will later compensate for the rounding error. + function calcAtAChi( + int256 x, + int256 y, + 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); + + // (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)); + } + + /** + * @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( + 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); + + // (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 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 + 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, + 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) + // 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) + ); + } + + /** + * @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, + 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) + // 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, + 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) + // 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)); + } + + /** + * @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, + 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); + // 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 because of rounding error. + val = val > 0 ? GyroPoolMath.sqrt(val.toUint256(), 5).toInt256() : int256(0); + } + + /** + * @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, + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived, + int256 invariant + ) 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); + 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. + 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 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))); + px = pgx.divDownMag(scalarProd(pc, mulA(params, IGyroECLPPool.Vector2(0, _ONE)))).toUint256(); + } + + /** + * @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( + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived, + IGyroECLPPool.Vector2 memory invariant, + int256 newBal, + uint8 assetIndex + ) internal pure { + if (assetIndex == 0) { + int256 xPlus = maxBalances0(params, derived, invariant); + require(newBal <= _MAX_BALANCES && newBal <= xPlus, AssetBoundsExceeded()); + } else { + int256 yPlus = maxBalances1(params, derived, invariant); + require(newBal <= _MAX_BALANCES && newBal <= yPlus, AssetBoundsExceeded()); + } + } + + function calcOutGivenIn( + uint256[] memory balances, + uint256 amountIn, + bool tokenInIsToken0, + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived, + IGyroECLPPool.Vector2 memory invariant + ) internal pure returns (uint256 amountOut) { + function( + int256, + IGyroECLPPool.EclpParams memory, + IGyroECLPPool.DerivedEclpParams memory, + IGyroECLPPool.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, + IGyroECLPPool.EclpParams memory params, + IGyroECLPPool.DerivedEclpParams memory derived, + IGyroECLPPool.Vector2 memory invariant + ) internal pure returns (uint256 amountIn) { + function( + int256, + IGyroECLPPool.EclpParams memory, + IGyroECLPPool.DerivedEclpParams memory, + IGyroECLPPool.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 + } + + 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 + // 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. Also, calculates an overestimate of calculated reserve + * post-swap. + */ + function solveQuadraticSwap( + int256 lambda, + int256 x, + int256 s, + int256 c, + 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. + 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. + 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). + 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 = 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. + + // 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 because 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; + } + + /** + * @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, + IGyroECLPPool.Vector2 memory r, // overestimate in x component, underestimate in y + int256 lambda, + int256 s, + int256 c, + IGyroECLPPool.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) + 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 + // 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; + } + + /** + * @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, + 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. + // Note that the error correction in the invariant should more than make up for uncaught rounding directions + // (in 38 decimals) in virtual offsets. + 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, + 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. + // Note that the error correction in the invariant should more than make up for uncaught rounding directions + // (in 38 decimals) in virtual offsets. + 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, + y, + params.c, + params.s, + r, + ba, + IGyroECLPPool.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..f6f97073f --- /dev/null +++ b/pkg/pool-gyro/contracts/lib/SignedFixedPoint.sol @@ -0,0 +1,277 @@ +// 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 */ + +/** + * @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(); + 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 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) { + // 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 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; + } + + /// @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 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; + + // 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 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; + } + + /// @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 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(); + + 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; + } + + /** + * @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(); + + return product / ONE_XP; + } + + /** + * @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; + } + + /** + * @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(); + + if (a == 0) { + return 0; + } + + int256 aInflated = a * ONE_XP; + if (aInflated / a != ONE_XP) revert DivInterval(); + + return aInflated / b; + } + + /** + * @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; + } + + /** + * @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; + 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; + } + + /** + * @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; + // 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; + } + + /** + * @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; + 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; + } + + /** + * @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). + int256 prod1 = a * b1; + int256 prod2 = a * b2; + return prod1 <= 0 && prod2 <= 0 ? (prod1 + prod2 / 1e19) / 1e19 : (prod1 + prod2 / 1e19 - 1) / 1e19 + 1; + } + + /** + * @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; + return ONE - x; + } +} 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 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/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/E2eBatchSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol new file mode 100644 index 000000000..ef96857df --- /dev/null +++ b/pkg/pool-gyro/test/foundry/E2eBatchSwapECLP.t.sol @@ -0,0 +1,42 @@ +// 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 { 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, bytes memory) { + IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); + return createGyroEclpPool(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; + + // 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/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/E2eSwapECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapECLP.t.sol new file mode 100644 index 000000000..9db0facd1 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/E2eSwapECLP.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 { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + +import { E2eSwapTest } from "@balancer-labs/v3-vault/test/foundry/E2eSwap.t.sol"; + +import { GyroEclpPoolDeployer } from "./utils/GyroEclpPoolDeployer.sol"; + +contract E2eSwapECLPTest is E2eSwapTest, GyroEclpPoolDeployer { + 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, bytes memory) { + IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); + return createGyroEclpPool(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/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/E2eSwapRateProviderECLP.t.sol b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol new file mode 100644 index 000000000..069bb1cd5 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/E2eSwapRateProviderECLP.t.sol @@ -0,0 +1,79 @@ +// 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 { GyroEclpPoolDeployer } from "./utils/GyroEclpPoolDeployer.sol"; + +contract E2eSwapRateProviderECLPTest is VaultContractsDeployer, E2eSwapRateProviderTest, GyroEclpPoolDeployer { + 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, bytes memory) { + 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 createGyroEclpPool(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/FungibilityGyroECLP.t.sol b/pkg/pool-gyro/test/foundry/FungibilityGyroECLP.t.sol new file mode 100644 index 000000000..b5d902914 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/FungibilityGyroECLP.t.sol @@ -0,0 +1,22 @@ +// 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, bytes memory) { + IRateProvider[] memory rateProviders = new IRateProvider[](tokens.length); + return createGyroEclpPool(tokens, rateProviders, label, vault, lp); + } +} 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..c8eac604e --- /dev/null +++ b/pkg/pool-gyro/test/foundry/LiquidityApproximationECLP.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +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"; + +import { GyroEclpPoolDeployer } from "./utils/GyroEclpPoolDeployer.sol"; + +contract LiquidityApproximationECLPTest is LiquidityApproximationTest, GyroEclpPoolDeployer { + function setUp() public virtual override { + LiquidityApproximationTest.setUp(); + + minSwapFeePercentage = IBasePool(swapPool).getMinimumSwapFeePercentage(); + + // The invariant of ECLP pools are smaller. + maxAmount = 1e5 * 1e18; + } + + 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 new file mode 100644 index 000000000..a5baed623 --- /dev/null +++ b/pkg/pool-gyro/test/foundry/utils/GyroEclpPoolDeployer.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +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 { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import { IVaultMock } from "@balancer-labs/v3-interfaces/contracts/test/IVaultMock.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"; + +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; + 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; + + 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, + string memory label, + IVaultMock vault, + address poolCreator + ) internal returns (address newPool, bytes memory poolArgs) { + GyroECLPPoolFactory factory = deployGyroECLPPoolFactory(vault); + + PoolRoleAccounts memory roleAccounts; + + // Avoids Stack-too-deep. + { + IGyroECLPPool.EclpParams memory params = IGyroECLPPool.EclpParams({ + alpha: _paramsAlpha, + beta: _paramsBeta, + c: _paramsC, + s: _paramsS, + lambda: _paramsLambda + }); + + IGyroECLPPool.DerivedEclpParams memory derivedParams = IGyroECLPPool.DerivedEclpParams({ + tauAlpha: IGyroECLPPool.Vector2(_tauAlphaX, _tauAlphaY), + tauBeta: IGyroECLPPool.Vector2(_tauBetaX, _tauBetaY), + u: _u, + v: _v, + w: _w, + z: _z, + dSq: _dSq + }); + + TokenConfig[] memory tokenConfig = vault.buildTokenConfig(tokens.asIERC20(), rateProviders); + + 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(newPool, label); + + // Cannot set the pool creator directly on a standard Balancer stable pool factory. + vault.manualSetPoolCreator(newPool, poolCreator); + + ProtocolFeeControllerMock feeController = ProtocolFeeControllerMock(address(vault.getProtocolFeeController())); + feeController.manualSetPoolCreator(newPool, poolCreator); + } + + function deployGyroECLPPoolFactory(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")); + } +} 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. diff --git a/pkg/vault/test/foundry/E2eBatchSwap.t.sol b/pkg/vault/test/foundry/E2eBatchSwap.t.sol index a27d3c6e3..3596ecad8 100644 --- a/pkg/vault/test/foundry/E2eBatchSwap.t.sol +++ b/pkg/vault/test/foundry/E2eBatchSwap.t.sol @@ -214,7 +214,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( diff --git a/pkg/vault/test/foundry/LiquidityApproximation.t.sol b/pkg/vault/test/foundry/LiquidityApproximation.t.sol index 79e47f0e1..7d8e2d31b 100644 --- a/pkg/vault/test/foundry/LiquidityApproximation.t.sol +++ b/pkg/vault/test/foundry/LiquidityApproximation.t.sol @@ -533,9 +533,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(), @@ -544,7 +544,6 @@ contract LiquidityApproximationTest is BaseVaultTest { bytes("") ); - vm.startPrank(alice); router.removeLiquiditySingleTokenExactOut( address(liquidityPool), IERC20(liquidityPool).balanceOf(alice), diff --git a/pvt/common/hardhat-base-config.ts b/pvt/common/hardhat-base-config.ts index 1181d76a3..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,25 +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-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, }, };