diff --git a/pkg/interfaces/contracts/pool-aclamm/IAclAmmPool.sol b/pkg/interfaces/contracts/pool-aclamm/IAclAmmPool.sol new file mode 100644 index 000000000..c28ce0eef --- /dev/null +++ b/pkg/interfaces/contracts/pool-aclamm/IAclAmmPool.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IBasePool } from "../vault/IBasePool.sol"; + +/// @dev Struct with data for deploying a new AclAmmPool. +struct AclAmmPoolParams { + string name; + string symbol; + string version; + uint256 increaseDayRate; + uint256 sqrtQ0; + uint256 centernessMargin; +} + +interface IAclAmmPool is IBasePool { + event SqrtQ0Updated(uint256 startSqrtQ0, uint256 endSqrtQ0, uint256 startTime, uint256 endTime); + event AclAmmPoolInitialized(uint256 increaseDayRate, uint256 sqrtQ0, uint256 centernessMargin); + + function getLastVirtualBalances() external view returns (uint256[] memory virtualBalances); + + function getLastTimestamp() external view returns (uint256); + + function getCurrentSqrtQ0() external view returns (uint256); + + function setSqrtQ0(uint256 newSqrtQ0, uint256 startTime, uint256 endTime) external; +} diff --git a/pkg/pool-aclamm/contracts/AclAmmPool.sol b/pkg/pool-aclamm/contracts/AclAmmPool.sol index 89aba5d89..f42a16793 100644 --- a/pkg/pool-aclamm/contracts/AclAmmPool.sol +++ b/pkg/pool-aclamm/contracts/AclAmmPool.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable not-rely-on-time pragma solidity ^0.8.24; @@ -6,9 +7,10 @@ import { ISwapFeePercentageBounds } from "@balancer-labs/v3-interfaces/contracts import { IUnbalancedLiquidityInvariantRatioBounds } from "@balancer-labs/v3-interfaces/contracts/vault/IUnbalancedLiquidityInvariantRatioBounds.sol"; -import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; +import { AclAmmPoolParams, IAclAmmPool } from "@balancer-labs/v3-interfaces/contracts/pool-aclamm/IAclAmmPool.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import { IHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IHooks.sol"; +import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; import { Rounding, PoolSwapParams, @@ -20,12 +22,21 @@ import { import { BalancerPoolToken } from "@balancer-labs/v3-vault/contracts/BalancerPoolToken.sol"; import { Version } from "@balancer-labs/v3-solidity-utils/contracts/helpers/Version.sol"; +import { BasePoolAuthentication } from "@balancer-labs/v3-pool-utils/contracts/BasePoolAuthentication.sol"; import { PoolInfo } from "@balancer-labs/v3-pool-utils/contracts/PoolInfo.sol"; import { BaseHooks } from "@balancer-labs/v3-vault/contracts/BaseHooks.sol"; -import { AclAmmMath } from "./lib/AclAmmMath.sol"; - -contract AclAmmPool is BalancerPoolToken, PoolInfo, Version, IBasePool, BaseHooks { +import { SqrtQ0State, AclAmmMath } from "./lib/AclAmmMath.sol"; + +contract AclAmmPool is + IUnbalancedLiquidityInvariantRatioBounds, + IAclAmmPool, + BalancerPoolToken, + PoolInfo, + BasePoolAuthentication, + Version, + BaseHooks +{ // uint256 private constant _MIN_SWAP_FEE_PERCENTAGE = 0.001e16; // 0.001% uint256 private constant _MIN_SWAP_FEE_PERCENTAGE = 0; uint256 private constant _MAX_SWAP_FEE_PERCENTAGE = 10e16; // 10% @@ -35,30 +46,27 @@ contract AclAmmPool is BalancerPoolToken, PoolInfo, Version, IBasePool, BaseHook // Invariant shrink limit: non-proportional remove cannot cause the invariant to decrease by less than this ratio. uint256 internal constant _MIN_INVARIANT_RATIO = 70e16; // 70% + SqrtQ0State private _sqrtQ0State; uint256 private _lastTimestamp; - uint256[] private _virtualBalances; - uint256 private _c; - uint256 private _sqrtQ0; uint256 private _centernessMargin; - - /// @dev Struct with data for deploying a new AclAmmPool. - struct AclAmmPoolParams { - string name; - string symbol; - string version; - uint256 increaseDayRate; - uint256 sqrtQ0; - uint256 centernessMargin; - } + uint256[] private _virtualBalances; constructor( AclAmmPoolParams memory params, IVault vault - ) BalancerPoolToken(vault, params.name, params.symbol) PoolInfo(vault) Version(params.version) { + ) + BalancerPoolToken(vault, params.name, params.symbol) + PoolInfo(vault) + BasePoolAuthentication(vault, msg.sender) + Version(params.version) + { _setIncreaseDayRate(params.increaseDayRate); - _setSqrtQ0(params.sqrtQ0); + + _sqrtQ0State.endSqrtQ0 = params.sqrtQ0; _setCenternessMargin(params.centernessMargin); + + emit AclAmmPoolInitialized(params.increaseDayRate, params.sqrtQ0, params.centernessMargin); } /// @inheritdoc IBasePool @@ -68,9 +76,10 @@ contract AclAmmPool is BalancerPoolToken, PoolInfo, Version, IBasePool, BaseHook balancesScaled18, _virtualBalances, _c, - _sqrtQ0, + _calculateCurrentSqrtQ0(), _lastTimestamp, _centernessMargin, + _sqrtQ0State, rounding ); } @@ -88,14 +97,19 @@ contract AclAmmPool is BalancerPoolToken, PoolInfo, Version, IBasePool, BaseHook request.balancesScaled18, _virtualBalances, _c, - _sqrtQ0, + _calculateCurrentSqrtQ0(), _lastTimestamp, - _centernessMargin + _centernessMargin, + block.timestamp, + _sqrtQ0State ); - _lastTimestamp = block.timestamp; if (changed) { _virtualBalances = virtualBalances; + + if (_sqrtQ0State.startTime != 0) { + _sqrtQ0State.startTime = 0; + } } // Calculate swap result @@ -131,14 +145,14 @@ contract AclAmmPool is BalancerPoolToken, PoolInfo, Version, IBasePool, BaseHook address, TokenConfig[] memory, LiquidityManagement calldata - ) public override returns (bool) { + ) public pure override returns (bool) { return true; } /// @inheritdoc IHooks function onBeforeInitialize(uint256[] memory balancesScaled18, bytes memory) public override returns (bool) { _lastTimestamp = block.timestamp; - _virtualBalances = AclAmmMath.initializeVirtualBalances(balancesScaled18, _sqrtQ0); + _virtualBalances = AclAmmMath.initializeVirtualBalances(balancesScaled18, _calculateCurrentSqrtQ0()); return true; } @@ -162,6 +176,7 @@ contract AclAmmPool is BalancerPoolToken, PoolInfo, Version, IBasePool, BaseHook return _MAX_INVARIANT_RATIO; } + /// @inheritdoc IAclAmmPool function getLastVirtualBalances() external view returns (uint256[] memory virtualBalances) { (, , uint256[] memory balancesScaled18, ) = _vault.getPoolTokenInfo(address(this)); @@ -170,22 +185,60 @@ contract AclAmmPool is BalancerPoolToken, PoolInfo, Version, IBasePool, BaseHook balancesScaled18, _virtualBalances, _c, - _sqrtQ0, + _calculateCurrentSqrtQ0(), _lastTimestamp, - _centernessMargin + _centernessMargin, + block.timestamp, + _sqrtQ0State ); } + /// @inheritdoc IAclAmmPool function getLastTimestamp() external view returns (uint256) { return _lastTimestamp; } - function _setIncreaseDayRate(uint256 increaseDayRate) internal { - _c = AclAmmMath.parseIncreaseDayRate(increaseDayRate); + /// @inheritdoc IAclAmmPool + function getCurrentSqrtQ0() external view override returns (uint256) { + return _calculateCurrentSqrtQ0(); } - function _setSqrtQ0(uint256 sqrtQ0) internal { - _sqrtQ0 = sqrtQ0; + /// @inheritdoc IAclAmmPool + function setSqrtQ0( + uint256 newSqrtQ0, + uint256 startTime, + uint256 endTime + ) external onlySwapFeeManagerOrGovernance(address(this)) { + _setSqrtQ0(newSqrtQ0, startTime, endTime); + } + + function _setSqrtQ0(uint256 endSqrtQ0, uint256 startTime, uint256 endTime) internal { + require(startTime < endTime, "AclAmmPool: Invalid time range"); + + uint256 startSqrtQ0 = _calculateCurrentSqrtQ0(); + _sqrtQ0State.startSqrtQ0 = startSqrtQ0; + _sqrtQ0State.endSqrtQ0 = endSqrtQ0; + _sqrtQ0State.startTime = startTime; + _sqrtQ0State.endTime = endTime; + + emit SqrtQ0Updated(startSqrtQ0, endSqrtQ0, startTime, endTime); + } + + function _calculateCurrentSqrtQ0() internal view returns (uint256) { + SqrtQ0State memory sqrtQ0State = _sqrtQ0State; + + return + AclAmmMath.calculateSqrtQ0( + block.timestamp, + sqrtQ0State.startSqrtQ0, + sqrtQ0State.endSqrtQ0, + sqrtQ0State.startTime, + sqrtQ0State.endTime + ); + } + + function _setIncreaseDayRate(uint256 increaseDayRate) internal { + _c = AclAmmMath.parseIncreaseDayRate(increaseDayRate); } function _setCenternessMargin(uint256 centernessMargin) internal { diff --git a/pkg/pool-aclamm/contracts/AclAmmPoolFactory.sol b/pkg/pool-aclamm/contracts/AclAmmPoolFactory.sol index 255fe1358..4633188e9 100644 --- a/pkg/pool-aclamm/contracts/AclAmmPoolFactory.sol +++ b/pkg/pool-aclamm/contracts/AclAmmPoolFactory.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.24; import { IPoolVersion } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IPoolVersion.sol"; import { IVaultErrors } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultErrors.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { AclAmmPoolParams } from "@balancer-labs/v3-interfaces/contracts/pool-aclamm/IAclAmmPool.sol"; import { TokenConfig, PoolRoleAccounts, @@ -74,7 +75,7 @@ contract AclAmmPoolFactory is IPoolVersion, BasePoolFactory, Version { pool = _create( abi.encode( - AclAmmPool.AclAmmPoolParams({ + AclAmmPoolParams({ name: name, symbol: symbol, version: _poolVersion, diff --git a/pkg/pool-aclamm/contracts/lib/AclAmmMath.sol b/pkg/pool-aclamm/contracts/lib/AclAmmMath.sol index 7bdcde217..65e99f34f 100644 --- a/pkg/pool-aclamm/contracts/lib/AclAmmMath.sol +++ b/pkg/pool-aclamm/contracts/lib/AclAmmMath.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable not-rely-on-time pragma solidity ^0.8.24; @@ -7,6 +8,13 @@ import { Rounding } from "@balancer-labs/v3-interfaces/contracts/vault/VaultType import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; import { LogExpMath } from "@balancer-labs/v3-solidity-utils/contracts/math/LogExpMath.sol"; +struct SqrtQ0State { + uint256 startSqrtQ0; + uint256 endSqrtQ0; + uint256 startTime; + uint256 endTime; +} + library AclAmmMath { using FixedPoint for uint256; @@ -17,6 +25,7 @@ library AclAmmMath { uint256 sqrtQ0, uint256 lastTimestamp, uint256 centernessMargin, + SqrtQ0State memory sqrtQ0State, Rounding rounding ) internal view returns (uint256) { function(uint256, uint256) pure returns (uint256) _mulUpOrDown = rounding == Rounding.ROUND_DOWN @@ -29,7 +38,9 @@ library AclAmmMath { c, sqrtQ0, lastTimestamp, - centernessMargin + centernessMargin, + block.timestamp, + sqrtQ0State ); return _mulUpOrDown((balancesScaled18[0] + virtualBalances[0]), (balancesScaled18[1] + virtualBalances[1])); @@ -84,9 +95,12 @@ library AclAmmMath { uint256 c, uint256 sqrtQ0, uint256 lastTimestamp, - uint256 centernessMargin + uint256 centernessMargin, + uint256 currentTime, + SqrtQ0State memory sqrtQ0State //TODO: optimize gas usage ) internal view returns (uint256[] memory virtualBalances, bool changed) { // TODO Review rounding + // TODO: try to find better way to change the virtual balances in storage virtualBalances = new uint256[](balancesScaled18.length); @@ -112,6 +126,24 @@ library AclAmmMath { } changed = true; + } else if (sqrtQ0State.startTime != 0 && currentTime > sqrtQ0State.startTime) { + uint256 rACenter = lastVirtualBalances[0].mulDown(sqrtQ0State.startSqrtQ0 - FixedPoint.ONE); + uint256 rBCenter = lastVirtualBalances[1].mulDown(sqrtQ0State.startSqrtQ0 - FixedPoint.ONE); + + uint256 currentSqrtQ0 = calculateSqrtQ0( + currentTime, + sqrtQ0State.startSqrtQ0, + sqrtQ0State.endSqrtQ0, + sqrtQ0State.startTime, + sqrtQ0State.endTime + ); + + virtualBalances[0] = rACenter.divDown(currentSqrtQ0 - FixedPoint.ONE); + virtualBalances[1] = rBCenter.divDown(currentSqrtQ0 - FixedPoint.ONE); + + if (currentTime >= sqrtQ0State.endTime) { + changed = true; + } } else { virtualBalances = lastVirtualBalances; } @@ -145,6 +177,25 @@ library AclAmmMath { } } + function calculateSqrtQ0( + uint256 currentTime, + uint256 startSqrtQ0, + uint256 endSqrtQ0, + uint256 startTime, + uint256 endTime + ) internal pure returns (uint256) { + if (currentTime <= startTime) { + return startSqrtQ0; + } else if (currentTime >= endTime) { + return endSqrtQ0; + } + + uint256 numerator = ((endTime - currentTime) * startSqrtQ0) + ((currentTime - startTime) * endSqrtQ0); + uint256 denominator = endTime - startTime; + + return numerator / denominator; + } + function isAboveCenter( uint256[] memory balancesScaled18, uint256[] memory virtualBalances @@ -157,6 +208,7 @@ library AclAmmMath { } function parseIncreaseDayRate(uint256 increaseDayRate) internal pure returns (uint256) { - return increaseDayRate / 110000; // Divide daily rate by a number of seconds per day (plus some adjustment) = 86400 + 25% + // Divide daily rate by a number of seconds per day (plus some adjustment) = 86400 + 25% + return increaseDayRate / 110000; } } diff --git a/pkg/pool-aclamm/contracts/test/AclAmmMathMock.sol b/pkg/pool-aclamm/contracts/test/AclAmmMathMock.sol new file mode 100644 index 000000000..1cc6c7307 --- /dev/null +++ b/pkg/pool-aclamm/contracts/test/AclAmmMathMock.sol @@ -0,0 +1,131 @@ +// 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 { SqrtQ0State, AclAmmMath } from "../lib/AclAmmMath.sol"; + +contract AclAmmMathMock { + function computeInvariant( + uint256[] memory balancesScaled18, + uint256[] memory lastVirtualBalances, + uint256 c, + uint256 sqrtQ0, + uint256 lastTimestamp, + uint256 centernessMargin, + SqrtQ0State memory sqrtQ0State, + Rounding rounding + ) external view returns (uint256) { + return + AclAmmMath.computeInvariant( + balancesScaled18, + lastVirtualBalances, + c, + sqrtQ0, + lastTimestamp, + centernessMargin, + sqrtQ0State, + rounding + ); + } + + function calculateOutGivenIn( + uint256[] memory balancesScaled18, + uint256[] memory virtualBalances, + uint256 tokenInIndex, + uint256 tokenOutIndex, + uint256 amountGivenScaled18 + ) external pure returns (uint256) { + return + AclAmmMath.calculateOutGivenIn( + balancesScaled18, + virtualBalances, + tokenInIndex, + tokenOutIndex, + amountGivenScaled18 + ); + } + + function calculateInGivenOut( + uint256[] memory balancesScaled18, + uint256[] memory virtualBalances, + uint256 tokenInIndex, + uint256 tokenOutIndex, + uint256 amountGivenScaled18 + ) external pure returns (uint256) { + return + AclAmmMath.calculateInGivenOut( + balancesScaled18, + virtualBalances, + tokenInIndex, + tokenOutIndex, + amountGivenScaled18 + ); + } + + function initializeVirtualBalances( + uint256[] memory balancesScaled18, + uint256 sqrtQ0 + ) external pure returns (uint256[] memory virtualBalances) { + return AclAmmMath.initializeVirtualBalances(balancesScaled18, sqrtQ0); + } + + function getVirtualBalances( + uint256[] memory balancesScaled18, + uint256[] memory lastVirtualBalances, + uint256 c, + uint256 sqrtQ0, + uint256 lastTimestamp, + uint256 centernessMargin, + uint256 currentTime, + SqrtQ0State memory sqrtQ0State + ) external view returns (uint256[] memory virtualBalances, bool changed) { + return + AclAmmMath.getVirtualBalances( + balancesScaled18, + lastVirtualBalances, + c, + sqrtQ0, + lastTimestamp, + centernessMargin, + currentTime, + sqrtQ0State + ); + } + + function isPoolInRange( + uint256[] memory balancesScaled18, + uint256[] memory virtualBalances, + uint256 centernessMargin + ) external pure returns (bool) { + return AclAmmMath.isPoolInRange(balancesScaled18, virtualBalances, centernessMargin); + } + + function calculateCenterness( + uint256[] memory balancesScaled18, + uint256[] memory virtualBalances + ) external pure returns (uint256) { + return AclAmmMath.calculateCenterness(balancesScaled18, virtualBalances); + } + + function calculateSqrtQ0( + uint256 currentTime, + uint256 startSqrtQ0, + uint256 endSqrtQ0, + uint256 startTime, + uint256 endTime + ) external pure returns (uint256) { + return AclAmmMath.calculateSqrtQ0(currentTime, startSqrtQ0, endSqrtQ0, startTime, endTime); + } + + function isAboveCenter( + uint256[] memory balancesScaled18, + uint256[] memory virtualBalances + ) external pure returns (bool) { + return AclAmmMath.isAboveCenter(balancesScaled18, virtualBalances); + } + + function parseIncreaseDayRate(uint256 increaseDayRate) external pure returns (uint256) { + return AclAmmMath.parseIncreaseDayRate(increaseDayRate); + } +} diff --git a/pkg/pool-aclamm/test/AclAmmMath.test.ts b/pkg/pool-aclamm/test/AclAmmMath.test.ts new file mode 100644 index 000000000..cb76b0b6f --- /dev/null +++ b/pkg/pool-aclamm/test/AclAmmMath.test.ts @@ -0,0 +1,45 @@ +import { Contract, BigNumberish } from 'ethers'; +import { deploy } from '@balancer-labs/v3-helpers/src/contract'; +import { expect } from 'chai'; +import { bn, fp } from '@balancer-labs/v3-helpers/src/numbers'; +import { expectEqualWithError } from '@balancer-labs/v3-helpers/src/test/relativeError'; +import { random } from 'lodash'; +import { calculateSqrtQ0 } from '@balancer-labs/v3-helpers/src/math/aclAmm'; + + +describe('AclAmmMath', function () { + let mock: Contract; + + before(async function () { + mock = await deploy('AclAmmMathMock'); + }); + + context('calculateSqrtQ0', () => { + it('should return endSqrtQ0Fp when currentTime > endTime', async () => { + const currentTime = 100; + const startSqrtQ0Fp = bn(100e18); + const endSqrtQ0Fp = bn(300e18); + const startTime = 1; + const endTime = 50; + + const contractResult = await mock.calculateSqrtQ0(currentTime, startSqrtQ0Fp, endSqrtQ0Fp, startTime, endTime); + const mathResult = calculateSqrtQ0(currentTime, startSqrtQ0Fp, endSqrtQ0Fp, startTime, endTime); + + expect(contractResult).to.equal(mathResult); + expect(contractResult).to.equal(endSqrtQ0Fp); + }); + + it('should return the correct value when currentTime < endTime', async () => { + const currentTime = 25; + const startSqrtQ0Fp = bn(100e18); + const endSqrtQ0Fp = bn(300e18); + const startTime = 1; + const endTime = 50; + + const contractResult = await mock.calculateSqrtQ0(currentTime, startSqrtQ0Fp, endSqrtQ0Fp, startTime, endTime); + const mathResult = calculateSqrtQ0(currentTime, startSqrtQ0Fp, endSqrtQ0Fp, startTime, endTime); + + expect(contractResult).to.equal(mathResult); + }); + }); +}); diff --git a/pkg/pool-aclamm/test/foundry/AclAmmMath.t.sol b/pkg/pool-aclamm/test/foundry/AclAmmMath.t.sol index 2cc65c859..6ae7de920 100644 --- a/pkg/pool-aclamm/test/foundry/AclAmmMath.t.sol +++ b/pkg/pool-aclamm/test/foundry/AclAmmMath.t.sol @@ -103,4 +103,54 @@ contract AclAmmMathTest is Test { assertEq(isAboveCenter, balance0.divDown(balance1) > virtualBalance0.divDown(virtualBalance1)); } } + + function testCalculateSqrtQ0__Fuzz( + uint256 currentTime, + uint256 startSqrtQ0, + uint256 endSqrtQ0, + uint256 startTime, + uint256 endTime + ) public pure { + endTime = bound(endTime, 2, type(uint64).max); + startTime = bound(startTime, 1, endTime - 1); + currentTime = bound(currentTime, startTime, endTime); + + endSqrtQ0 = bound(endSqrtQ0, 1, type(uint128).max); + startSqrtQ0 = bound(endSqrtQ0, 1, type(uint128).max); + + uint256 sqrtQ0 = AclAmmMath.calculateSqrtQ0(currentTime, startSqrtQ0, endSqrtQ0, startTime, endTime); + + currentTime++; + uint256 nextSqrtQ0 = AclAmmMath.calculateSqrtQ0(currentTime, startSqrtQ0, endSqrtQ0, startTime, endTime); + + if (startSqrtQ0 >= endSqrtQ0) { + assertLe(nextSqrtQ0, sqrtQ0, "Next sqrtQ0 should be less than current sqrtQ0"); + } else { + assertGe(nextSqrtQ0, sqrtQ0, "Next sqrtQ0 should be greater than current sqrtQ0"); + } + } + + function testCalculateSqrtQ0WhenCurrentTimeIsAfterEndTime() public pure { + uint256 startSqrtQ0 = 100; + uint256 endSqrtQ0 = 200; + uint256 startTime = 0; + uint256 endTime = 50; + uint256 currentTime = 100; + + uint256 sqrtQ0 = AclAmmMath.calculateSqrtQ0(currentTime, startSqrtQ0, endSqrtQ0, startTime, endTime); + + assertEq(sqrtQ0, endSqrtQ0, "SqrtQ0 should be equal to endSqrtQ0"); + } + + function testCalculateSqrtQ0WhenCurrentTimeIsBeforeStartTime() public pure { + uint256 startSqrtQ0 = 100; + uint256 endSqrtQ0 = 200; + uint256 startTime = 50; + uint256 endTime = 100; + uint256 currentTime = 0; + + uint256 sqrtQ0 = AclAmmMath.calculateSqrtQ0(currentTime, startSqrtQ0, endSqrtQ0, startTime, endTime); + + assertEq(sqrtQ0, startSqrtQ0, "SqrtQ0 should be equal to startSqrtQ0"); + } } diff --git a/pkg/pool-aclamm/test/foundry/AclAmmPool.t.sol b/pkg/pool-aclamm/test/foundry/AclAmmPool.t.sol index fcf507c31..fc1230ed7 100644 --- a/pkg/pool-aclamm/test/foundry/AclAmmPool.t.sol +++ b/pkg/pool-aclamm/test/foundry/AclAmmPool.t.sol @@ -11,6 +11,7 @@ import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/Fixe import { BaseAclAmmTest } from "./utils/BaseAclAmmTest.sol"; import { AclAmmPool } from "../../contracts/AclAmmPool.sol"; +import { AclAmmMath } from "../../contracts/lib/AclAmmMath.sol"; contract AclAmmPoolTest is BaseAclAmmTest { using FixedPoint for uint256; @@ -54,7 +55,7 @@ contract AclAmmPoolTest is BaseAclAmmTest { if (swapAmount != 0) { vm.prank(bob); - uint256 amountOut = router.swapSingleTokenExactIn( + router.swapSingleTokenExactIn( pool, IERC20(tokenInIndex == daiIdx ? dai : usdc), IERC20(tokenOutIndex == daiIdx ? dai : usdc), @@ -86,7 +87,7 @@ contract AclAmmPoolTest is BaseAclAmmTest { uint256[] memory virtualBalances = AclAmmPool(pool).getLastVirtualBalances(); (, , uint256[] memory balances, ) = vault.getPoolTokenInfo(pool); - uint256 currentPoolPriceDai = _getCurrentDaiPoolPrice(); + // uint256 currentPoolPriceDai = _getCurrentDaiPoolPrice(); console2.log("balances[0]: %s", balances[0]); console2.log("balances[1]: %s", balances[1]); @@ -138,4 +139,30 @@ contract AclAmmPoolTest is BaseAclAmmTest { counter++; return uint256(keccak256(abi.encodePacked(block.prevrandao, block.timestamp, counter))); } + + function testGetCurrentSqrtQ0() public view { + uint256 sqrtQ0 = AclAmmPool(pool).getCurrentSqrtQ0(); + assertEq(sqrtQ0, _DEFAULT_SQRT_Q0, "Invalid default sqrtQ0"); + } + + function testSetSqrtQ0() public { + uint256 newSqrtQ0 = 2e18; + uint256 startTime = block.timestamp; + uint256 duration = 1 hours; + uint256 endTime = block.timestamp + duration; + + uint256 startSqrtQ0 = AclAmmPool(pool).getCurrentSqrtQ0(); + vm.prank(admin); + AclAmmPool(pool).setSqrtQ0(newSqrtQ0, startTime, endTime); + + skip(duration / 2); + uint256 sqrtQ0 = AclAmmPool(pool).getCurrentSqrtQ0(); + uint256 mathSqrtQ0 = AclAmmMath.calculateSqrtQ0(block.timestamp, startSqrtQ0, newSqrtQ0, startTime, endTime); + + assertEq(sqrtQ0, mathSqrtQ0, "SqrtQ0 not updated correctly"); + + skip(duration / 2 + 1); + sqrtQ0 = AclAmmPool(pool).getCurrentSqrtQ0(); + assertEq(sqrtQ0, newSqrtQ0, "SqrtQ0 does not match new value"); + } } diff --git a/pkg/pool-aclamm/test/foundry/AclAmmPoolVirtualBalances.t.sol b/pkg/pool-aclamm/test/foundry/AclAmmPoolVirtualBalances.t.sol new file mode 100644 index 000000000..769070a6e --- /dev/null +++ b/pkg/pool-aclamm/test/foundry/AclAmmPoolVirtualBalances.t.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { console } from "forge-std/Test.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { GyroPoolMath } from "@balancer-labs/v3-pool-gyro/contracts/lib/GyroPoolMath.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol"; +import { Rounding } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { IAclAmmPool } from "@balancer-labs/v3-interfaces/contracts/pool-aclamm/IAclAmmPool.sol"; + +import { BaseAclAmmTest } from "./utils/BaseAclAmmTest.sol"; +import { AclAmmPool } from "../../contracts/AclAmmPool.sol"; +import { AclAmmMath } from "../../contracts/lib/AclAmmMath.sol"; + +contract AclAmmPoolVirtualBalancesTest is BaseAclAmmTest { + using FixedPoint for uint256; + using ArrayHelpers for *; + + uint256 internal constant maxPrice = 4000; + uint256 internal constant minPrice = 2000; + uint256 internal constant initialABalance = 1_000_000e18; + uint256 internal constant initialBBalance = 100_000e18; + + function setUp() public virtual override { + setSqrtQ0(minPrice, maxPrice); + setInitialBalances(initialABalance, initialBBalance); + setIncreaseDayRate(0); + super.setUp(); + } + + function testInitialParams() public view { + uint256[] memory virtualBalances = _calculateVirtualBalances(); + + uint256[] memory curentVirtualBalances = AclAmmPool(pool).getLastVirtualBalances(); + + assertEq(AclAmmPool(pool).getCurrentSqrtQ0(), sqrtQ0(), "Invalid sqrtQ0"); + assertEq(curentVirtualBalances[0], virtualBalances[0], "Invalid virtual A balance"); + assertEq(curentVirtualBalances[1], virtualBalances[1], "Invalid virtual B balance"); + } + + function testWithDifferentInitialBalances_Fuzz(int256 diffCoefficient) public { + // This test verifies the virtual balances of two pools, where the real balances + // differ by a certain coefficient while maintaining the balance ratio. + + diffCoefficient = bound(diffCoefficient, -100, 100); + if (diffCoefficient >= -1 && diffCoefficient <= 1) { + diffCoefficient = 2; + } + + uint256[] memory newInitialBalances = new uint256[](2); + if (diffCoefficient > 0) { + newInitialBalances[0] = initialABalance * uint256(diffCoefficient); + newInitialBalances[1] = initialBBalance * uint256(diffCoefficient); + } else { + newInitialBalances[0] = initialABalance / uint256(-diffCoefficient); + newInitialBalances[1] = initialBBalance / uint256(-diffCoefficient); + } + + setInitialBalances(newInitialBalances[0], newInitialBalances[1]); + (address firstPool, address secondPool) = _createNewPool(); + + assertEq(AclAmmPool(firstPool).getCurrentSqrtQ0(), sqrtQ0(), "Invalid sqrtQ0 for firstPool"); + assertEq(AclAmmPool(secondPool).getCurrentSqrtQ0(), sqrtQ0(), "Invalid sqrtQ0 for newPool"); + + uint256[] memory curentFirstPoolVirtualBalances = AclAmmPool(firstPool).getLastVirtualBalances(); + uint256[] memory curentNewPoolVirtualBalances = AclAmmPool(secondPool).getLastVirtualBalances(); + + if (diffCoefficient > 0) { + assertGt( + curentNewPoolVirtualBalances[0], + curentFirstPoolVirtualBalances[0], + "Virtual A balance should be greater for newPool" + ); + assertGt( + curentNewPoolVirtualBalances[1], + curentFirstPoolVirtualBalances[1], + "Virtual B balance should be greater for newPool" + ); + } else { + assertLt( + curentNewPoolVirtualBalances[0], + curentFirstPoolVirtualBalances[0], + "Virtual A balance should be less for newPool" + ); + assertLt( + curentNewPoolVirtualBalances[1], + curentFirstPoolVirtualBalances[1], + "Virtual B balance should be less for newPool" + ); + } + } + + function testWithDifferentPriceRange_Fuzz(uint256 newSqrtQ) public { + newSqrtQ = bound(newSqrtQ, 1.4e18, 1_000_000e18); + + uint256 initialSqrtQ = sqrtQ0(); + setSqrtQ0(newSqrtQ); + (address firstPool, address secondPool) = _createNewPool(); + + uint256[] memory curentFirstPoolVirtualBalances = AclAmmPool(firstPool).getLastVirtualBalances(); + uint256[] memory curentNewPoolVirtualBalances = AclAmmPool(secondPool).getLastVirtualBalances(); + + if (newSqrtQ > initialSqrtQ) { + assertLt( + curentNewPoolVirtualBalances[0], + curentFirstPoolVirtualBalances[0], + "Virtual A balance should be less for newPool" + ); + assertLt( + curentNewPoolVirtualBalances[1], + curentFirstPoolVirtualBalances[1], + "Virtual B balance should be less for newPool" + ); + } else { + assertGe( + curentNewPoolVirtualBalances[0], + curentFirstPoolVirtualBalances[0], + "Virtual A balance should be greater for newPool" + ); + assertGe( + curentNewPoolVirtualBalances[1], + curentFirstPoolVirtualBalances[1], + "Virtual B balance should be greater for newPool" + ); + } + } + + function testChangingDifferentPriceRange_Fuzz(uint256 newSqrtQ) public { + newSqrtQ = bound(newSqrtQ, 1.4e18, 1_000_000e18); + + uint256 initialSqrtQ = sqrtQ0(); + + uint256 duration = 2 hours; + + uint256[] memory poolVirtualBalancesBefore = AclAmmPool(pool).getLastVirtualBalances(); + + uint256 currentTimestamp = block.timestamp; + + vm.prank(admin); + AclAmmPool(pool).setSqrtQ0(initialSqrtQ, currentTimestamp, currentTimestamp + duration); + skip(duration); + + uint256[] memory poolVirtualBalancesAfter = AclAmmPool(pool).getLastVirtualBalances(); + + if (newSqrtQ > initialSqrtQ) { + assertLt( + poolVirtualBalancesAfter[0], + poolVirtualBalancesBefore[0], + "Virtual A balance after should be less than before" + ); + assertLt( + poolVirtualBalancesAfter[1], + poolVirtualBalancesBefore[1], + "Virtual B balance after should be less than before" + ); + } else { + assertGe( + poolVirtualBalancesAfter[0], + poolVirtualBalancesBefore[0], + "Virtual A balance after should be greater than before" + ); + assertGe( + poolVirtualBalancesAfter[1], + poolVirtualBalancesBefore[1], + "Virtual B balance after should be greater than before" + ); + } + } + + function testSwap_Fuzz(uint256 exactAmountIn) public { + exactAmountIn = bound(exactAmountIn, 1e18, 10_000e18); + + uint256[] memory virtualBalances = _calculateVirtualBalances(); + uint256 invariantBefore = _getCurrentInvariant(); + + vm.prank(alice); + router.swapSingleTokenExactIn(pool, dai, usdc, exactAmountIn, 1, UINT256_MAX, false, new bytes(0)); + + uint256 invariantAfter = _getCurrentInvariant(); + assertEq(invariantBefore, invariantAfter, "Invariant should not change"); + + uint256[] memory curentVirtualBalances = AclAmmPool(pool).getLastVirtualBalances(); + assertEq(curentVirtualBalances[0], virtualBalances[0], "Virtual A balances don't equal"); + assertEq(curentVirtualBalances[1], virtualBalances[1], "Virtual B balances don't equal"); + } + + function testAddLiquidity_Fuzz(uint256 exactBptAmountOut) public { + exactBptAmountOut = bound(exactBptAmountOut, 1e18, 10_000e18); + + uint256 invariantBefore = _getCurrentInvariant(); + + vm.prank(alice); + router.addLiquidityProportional( + pool, + [MAX_UINT128, MAX_UINT128].toMemoryArray(), + exactBptAmountOut, + false, + new bytes(0) + ); + + uint256 invariantAfter = _getCurrentInvariant(); + + assertGt(invariantAfter, invariantBefore, "Invariant should increase"); + + // TODO: add check for virtual balances + } + + function testRemoveLiquidity_Fuzz(uint256 exactBptAmountIn) public { + exactBptAmountIn = bound(exactBptAmountIn, 1e18, 10_000e18); + + uint256 invariantBefore = _getCurrentInvariant(); + + vm.prank(lp); + router.removeLiquidityProportional( + pool, + exactBptAmountIn, + [uint256(1), 1].toMemoryArray(), + false, + new bytes(0) + ); + + uint256 invariantAfter = _getCurrentInvariant(); + assertLt(invariantAfter, invariantBefore, "Invariant should decrease"); + + // TODO: add check for virtual balances + } + + function _getCurrentInvariant() internal view returns (uint256) { + (, , uint256[] memory balances, ) = vault.getPoolTokenInfo(pool); + return AclAmmPool(pool).computeInvariant(balances, Rounding.ROUND_DOWN); + } + + function _calculateVirtualBalances() internal view returns (uint256[] memory virtualBalances) { + virtualBalances = new uint256[](2); + + uint256 sqrtQMinusOne = sqrtQ0() - FixedPoint.ONE; + virtualBalances[0] = initialABalance.divDown(sqrtQMinusOne); + virtualBalances[1] = initialBBalance.divDown(sqrtQMinusOne); + } + + function _createNewPool() internal returns (address initalPool, address newPool) { + initalPool = pool; + salt = keccak256(abi.encodePacked("test")); + (pool, poolArguments) = createPool(); + approveForPool(IERC20(pool)); + initPool(); + newPool = pool; + } +} diff --git a/pkg/pool-aclamm/test/foundry/utils/BaseAclAmmTest.sol b/pkg/pool-aclamm/test/foundry/utils/BaseAclAmmTest.sol index 279e1d807..8677bea9a 100644 --- a/pkg/pool-aclamm/test/foundry/utils/BaseAclAmmTest.sol +++ b/pkg/pool-aclamm/test/foundry/utils/BaseAclAmmTest.sol @@ -7,12 +7,15 @@ import "forge-std/Test.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { PoolRoleAccounts, LiquidityManagement } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { AclAmmPoolParams } from "@balancer-labs/v3-interfaces/contracts/pool-aclamm/IAclAmmPool.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; import { PoolFactoryMock } from "@balancer-labs/v3-vault/contracts/test/PoolFactoryMock.sol"; +import { GyroPoolMath } from "@balancer-labs/v3-pool-gyro/contracts/lib/GyroPoolMath.sol"; import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol"; import { AclAmmPoolContractsDeployer } from "./AclAmmPoolContractsDeployer.sol"; @@ -20,6 +23,7 @@ import { AclAmmPool } from "../../../contracts/AclAmmPool.sol"; import { AclAmmPoolFactory } from "../../../contracts/AclAmmPoolFactory.sol"; contract BaseAclAmmTest is AclAmmPoolContractsDeployer, BaseVaultTest { + using FixedPoint for uint256; using CastingHelpers for address[]; using ArrayHelpers for *; @@ -31,6 +35,12 @@ contract BaseAclAmmTest is AclAmmPoolContractsDeployer, BaseVaultTest { uint256 internal constant _DEFAULT_SQRT_Q0 = 1.41421356e18; // Price Range of 4 (fourth square root is 1.41) uint256 internal constant _DEFAULT_CENTERNESS_MARGIN = 10e16; // 10% + uint256 private _sqrtQ0 = _DEFAULT_SQRT_Q0; + uint256 private _increaseDayRate = _DEFAULT_INCREASE_DAY_RATE; + uint256[] private _initialBalances = new uint256[](2); + + bytes32 internal salt = ZERO_BYTES32; + AclAmmPool internal ammPool; AclAmmPoolFactory internal factory; @@ -38,11 +48,42 @@ contract BaseAclAmmTest is AclAmmPoolContractsDeployer, BaseVaultTest { uint256 internal usdcIdx; function setUp() public virtual override { + if (_initialBalances[0] == 0 && _initialBalances[1] == 0) { + setInitialBalances(poolInitAmount, poolInitAmount); + } + super.setUp(); (daiIdx, usdcIdx) = getSortedIndexes(address(dai), address(usdc)); } + function setSqrtQ0(uint256 minPrice, uint256 maxPrice) internal { + uint256 doubleQ0 = maxPrice.divDown(minPrice); + uint256 Q0 = GyroPoolMath.sqrt(doubleQ0, 5); + _sqrtQ0 = GyroPoolMath.sqrt(Q0, 5); + } + + function setSqrtQ0(uint256 sqrtQ0_) internal { + _sqrtQ0 = sqrtQ0_; + } + + function sqrtQ0() internal view returns (uint256) { + return _sqrtQ0; + } + + function setIncreaseDayRate(uint256 increaseDayRate) internal { + _increaseDayRate = increaseDayRate; + } + + function setInitialBalances(uint256 aBalance, uint256 bBalance) internal { + _initialBalances[0] = aBalance; + _initialBalances[1] = bBalance; + } + + function initialBalances() internal view returns (uint256[] memory) { + return _initialBalances; + } + function createPoolFactory() internal override returns (address) { factory = deployAclAmmPoolFactory(vault, 365 days, "Factory v1", _POOL_VERSION); vm.label(address(factory), "Acl Amm Factory"); @@ -61,6 +102,8 @@ contract BaseAclAmmTest is AclAmmPoolContractsDeployer, BaseVaultTest { PoolRoleAccounts memory roleAccounts; + roleAccounts = PoolRoleAccounts({ pauseManager: address(0), swapFeeManager: admin, poolCreator: address(0) }); + newPool = AclAmmPoolFactory(poolFactory).create( name, symbol, @@ -68,23 +111,29 @@ contract BaseAclAmmTest is AclAmmPoolContractsDeployer, BaseVaultTest { roleAccounts, _DEFAULT_SWAP_FEE, _DEFAULT_INCREASE_DAY_RATE, - _DEFAULT_SQRT_Q0, + sqrtQ0(), _DEFAULT_CENTERNESS_MARGIN, - ZERO_BYTES32 + salt ); vm.label(newPool, label); // poolArgs is used to check pool deployment address with create2. poolArgs = abi.encode( - AclAmmPool.AclAmmPoolParams({ + AclAmmPoolParams({ name: name, symbol: symbol, version: _POOL_VERSION, increaseDayRate: _DEFAULT_INCREASE_DAY_RATE, - sqrtQ0: _DEFAULT_SQRT_Q0, + sqrtQ0: sqrtQ0(), centernessMargin: _DEFAULT_CENTERNESS_MARGIN }), vault ); } + + function initPool() internal virtual override { + vm.startPrank(lp); + _initPool(pool, _initialBalances, 0); + vm.stopPrank(); + } } diff --git a/pvt/helpers/src/math/aclAmm.ts b/pvt/helpers/src/math/aclAmm.ts new file mode 100644 index 000000000..f2de0b9fe --- /dev/null +++ b/pvt/helpers/src/math/aclAmm.ts @@ -0,0 +1,19 @@ +import { BigNumberish } from 'ethers'; +import { bn } from '../numbers'; + +export function calculateSqrtQ0( + currentTime: number, + startSqrtQ0Fp: BigNumberish, + endSqrtQ0Fp: BigNumberish, + startTime: number, + endTime: number +): bigint { + if (currentTime > endTime) { + return bn(endSqrtQ0Fp); + } + + const numerator = bn(endTime - currentTime) * bn(startSqrtQ0Fp) + bn(currentTime - startTime) * bn(endSqrtQ0Fp); + const denominator = bn(endTime - startTime); + + return numerator / denominator; +}