diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index 0bb6659..f7b2f3a 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -22,28 +22,19 @@ runs:
       with:
         path: './node_modules/*'
         key: yarn-pool-${{ hashFiles('./yarn.lock') }}
-    - name: Cache library node modules
+        restore-keys: |
+          yarn-pool-
+    - name: Cache library pkg
       uses: actions/cache@v4
-      id: cache-lib-modules
+      id: cache-lib-pkg
       with:
-        path: './lib/balancer-v3-monorepo/**/node_modules/*'
-        key: lib-modules-${{ hashFiles('./submodule-hash.txt') }}
-    - name: Cache library artifacts
-      uses: actions/cache@v4
-      id: cache-lib-artifacts
-      with:
-        path: './lib/balancer-v3-monorepo/pkg/**/artifacts/*'
+        path: './lib/balancer-v3-monorepo/pkg'
         key: lib-pkg-${{ hashFiles('./submodule-hash.txt') }}
-    - name: Cache library typechain
-      uses: actions/cache@v4
-      id: cache-lib-typechain
-      with:
-        path: './lib/balancer-v3-monorepo/pkg/**/typechain-types/*'
-        key: lib-typechain-${{ hashFiles('./submodule-hash.txt') }}
+        restore-keys: |
+          lib-pkg-
     - name: Install lcov
       shell: bash
       run: sudo apt-get install lcov
     - name: Install fresh
       shell: bash
       run: sh ./scripts/install-fresh.sh
-      if: steps.cache-lib-artifacts.outputs.cache-hit != 'true' || steps.cache-lib-modules.outputs.cache-hit != 'true' || steps.cache-lib-typechain.outputs.cache-hit != 'true'
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a286bb7..a30b486 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,36 +24,12 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           FORGE_SNAPSHOT_CHECK: true
-      - uses: actions/upload-artifact@v4
-        with:
-          name: built-artifacts
-          path: artifacts/
-      - uses: actions/upload-artifact@v4
-        with:
-          name: built-lib
-          path: lib/balancer-v3-monorepo/pkg/
-      - uses: actions/upload-artifact@v4
-        with:
-          name: built-lib-pvt
-          path: lib/balancer-v3-monorepo/pvt/
 
   test-forge:
     runs-on: ubuntu-latest
     needs: lint-and-build
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/download-artifact@v4
-        with:
-          name: built-artifacts
-          path: artifacts/
-      - uses: actions/download-artifact@v4
-        with:
-          name: built-lib
-          path: lib/balancer-v3-monorepo/pkg/
-      - uses: actions/download-artifact@v4
-        with:
-          name: built-lib-pvt
-          path: lib/balancer-v3-monorepo/pvt/
       - name: Set up environment
         uses: ./.github/actions/setup
       - name: Test
@@ -66,17 +42,5 @@ jobs:
       - uses: actions/checkout@v4
       - name: Set up environment
         uses: ./.github/actions/setup
-      - uses: actions/download-artifact@v4
-        with:
-          name: built-artifacts
-          path: artifacts/
-      - uses: actions/download-artifact@v4
-        with:
-          name: built-lib
-          path: lib/balancer-v3-monorepo/pkg/
-      - uses: actions/download-artifact@v4
-        with:
-          name: built-lib-pvt
-          path: lib/balancer-v3-monorepo/pvt/
       - name: Test
         run: yarn test:hardhat
diff --git a/contracts/interfaces/IAclAmmPool.sol b/contracts/interfaces/IAclAmmPool.sol
index 9b51dab..40d4ba3 100644
--- a/contracts/interfaces/IAclAmmPool.sol
+++ b/contracts/interfaces/IAclAmmPool.sol
@@ -2,8 +2,6 @@
 
 pragma solidity ^0.8.24;
 
-import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
-
 import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol";
 
 /// @dev Struct with data for deploying a new AclAmmPool.
diff --git a/contracts/lib/AclAmmMath.sol b/contracts/lib/AclAmmMath.sol
index d5e3aa6..fbc10d6 100644
--- a/contracts/lib/AclAmmMath.sol
+++ b/contracts/lib/AclAmmMath.sol
@@ -64,9 +64,9 @@ library AclAmmMath {
         finalBalances[0] = balancesScaled18[0] + virtualBalances[0];
         finalBalances[1] = balancesScaled18[1] + virtualBalances[1];
 
-        uint256 invariant = finalBalances[0].mulDown(finalBalances[1]);
+        uint256 invariant = finalBalances[0].mulUp(finalBalances[1]);
 
-        return finalBalances[tokenOutIndex] - invariant.divDown(finalBalances[tokenInIndex] + amountGivenScaled18);
+        return finalBalances[tokenOutIndex] - invariant.divUp(finalBalances[tokenInIndex] + amountGivenScaled18);
     }
 
     function calculateInGivenOut(
@@ -109,6 +109,12 @@ library AclAmmMath {
 
         virtualBalances = lastVirtualBalances;
 
+        // If the last timestamp is the same as the current timestamp, virtual balances were already reviewed in the
+        // current block.
+        if (lastTimestamp == block.timestamp) {
+            return (virtualBalances, false);
+        }
+
         // Calculate currentSqrtQ0
         uint256 currentSqrtQ0 = calculateSqrtQ0(
             currentTimestamp,
@@ -118,30 +124,6 @@ library AclAmmMath {
             sqrtQ0State.endTime
         );
 
-        if (isPoolInRange(balancesScaled18, lastVirtualBalances, centerednessMargin) == false) {
-            uint256 q0 = currentSqrtQ0.mulDown(currentSqrtQ0);
-
-            if (isAboveCenter(balancesScaled18, lastVirtualBalances)) {
-                virtualBalances[1] = lastVirtualBalances[1].mulDown(
-                    LogExpMath.pow(FixedPoint.ONE - c, (block.timestamp - lastTimestamp) * FixedPoint.ONE)
-                );
-                // Va = (Ra * (Vb + Rb)) / (((Q0 - 1) * Vb) - Rb)
-                virtualBalances[0] = (balancesScaled18[0].mulDown(virtualBalances[1] + balancesScaled18[1])).divDown(
-                    (q0 - FixedPoint.ONE).mulDown(virtualBalances[1]) - balancesScaled18[1]
-                );
-            } else {
-                virtualBalances[0] = lastVirtualBalances[0].mulDown(
-                    LogExpMath.pow(FixedPoint.ONE - c, (block.timestamp - lastTimestamp) * FixedPoint.ONE)
-                );
-                // Vb = (Rb * (Va + Ra)) / (((Q0 - 1) * Va) - Ra)
-                virtualBalances[1] = (balancesScaled18[1].mulDown(virtualBalances[0] + balancesScaled18[0])).divDown(
-                    (q0 - FixedPoint.ONE).mulDown(virtualBalances[0]) - balancesScaled18[0]
-                );
-            }
-
-            changed = true;
-        }
-
         if (
             sqrtQ0State.startTime != 0 &&
             currentTimestamp > sqrtQ0State.startTime &&
@@ -170,6 +152,30 @@ library AclAmmMath {
 
             changed = true;
         }
+
+        if (isPoolInRange(balancesScaled18, lastVirtualBalances, centerednessMargin) == false) {
+            uint256 q0 = currentSqrtQ0.mulDown(currentSqrtQ0);
+
+            if (isAboveCenter(balancesScaled18, lastVirtualBalances)) {
+                virtualBalances[1] = lastVirtualBalances[1].mulDown(
+                    LogExpMath.pow(FixedPoint.ONE - c, (block.timestamp - lastTimestamp) * FixedPoint.ONE)
+                );
+                // Va = (Ra * (Vb + Rb)) / (((Q0 - 1) * Vb) - Rb)
+                virtualBalances[0] = (balancesScaled18[0].mulDown(virtualBalances[1] + balancesScaled18[1])).divDown(
+                    (q0 - FixedPoint.ONE).mulDown(virtualBalances[1]) - balancesScaled18[1]
+                );
+            } else {
+                virtualBalances[0] = lastVirtualBalances[0].mulDown(
+                    LogExpMath.pow(FixedPoint.ONE - c, (block.timestamp - lastTimestamp) * FixedPoint.ONE)
+                );
+                // Vb = (Rb * (Va + Ra)) / (((Q0 - 1) * Va) - Ra)
+                virtualBalances[1] = (balancesScaled18[1].mulDown(virtualBalances[0] + balancesScaled18[0])).divDown(
+                    (q0 - FixedPoint.ONE).mulDown(virtualBalances[0]) - balancesScaled18[0]
+                );
+            }
+
+            changed = true;
+        }
     }
 
     function isPoolInRange(
diff --git a/package.json b/package.json
index 86bd440..3f0ad70 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,7 @@
     "prettier": "npx prettier --write --plugin=prettier-plugin-solidity 'contracts/**/*.sol' 'test/**/*.sol'",
     "test": "yarn test:hardhat && yarn test:forge",
     "test:hardhat": "hardhat test",
-    "test:forge": "forge test --ffi -vvv"
+    "test:forge": "yarn build && REUSING_HARDHAT_ARTIFACTS=true forge test --ffi -vvv"
   },
   "packageManager": "yarn@4.0.0-rc.42",
   "dependencies": {
diff --git a/test/foundry/AclAmmPoolVirtualBalances.t.sol b/test/foundry/AclAmmPoolVirtualBalances.t.sol
index 7e4317f..1fe4cc8 100644
--- a/test/foundry/AclAmmPoolVirtualBalances.t.sol
+++ b/test/foundry/AclAmmPoolVirtualBalances.t.sol
@@ -2,8 +2,6 @@
 
 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";
@@ -20,14 +18,13 @@ 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;
+    uint256 private constant _PRICE_RANGE = 2e18; // Max price is 2x min price.
+    uint256 private constant _INITIAL_BALANCE_A = 1_000_000e18;
+    uint256 private constant _INITIAL_BALANCE_B = 100_000e18;
 
     function setUp() public virtual override {
-        setSqrtQ0(minPrice, maxPrice);
-        setInitialBalances(initialABalance, initialBBalance);
+        setPriceRange(_PRICE_RANGE);
+        setInitialBalances(_INITIAL_BALANCE_A, _INITIAL_BALANCE_B);
         setIncreaseDayRate(0);
         super.setUp();
     }
@@ -53,11 +50,11 @@ contract AclAmmPoolVirtualBalancesTest is BaseAclAmmTest {
 
         uint256[] memory newInitialBalances = new uint256[](2);
         if (diffCoefficient > 0) {
-            newInitialBalances[0] = initialABalance * uint256(diffCoefficient);
-            newInitialBalances[1] = initialBBalance * uint256(diffCoefficient);
+            newInitialBalances[0] = _INITIAL_BALANCE_A * uint256(diffCoefficient);
+            newInitialBalances[1] = _INITIAL_BALANCE_B * uint256(diffCoefficient);
         } else {
-            newInitialBalances[0] = initialABalance / uint256(-diffCoefficient);
-            newInitialBalances[1] = initialBBalance / uint256(-diffCoefficient);
+            newInitialBalances[0] = _INITIAL_BALANCE_A / uint256(-diffCoefficient);
+            newInitialBalances[1] = _INITIAL_BALANCE_B / uint256(-diffCoefficient);
         }
 
         setInitialBalances(newInitialBalances[0], newInitialBalances[1]);
@@ -94,17 +91,17 @@ contract AclAmmPoolVirtualBalancesTest is BaseAclAmmTest {
         }
     }
 
-    function testWithDifferentPriceRange_Fuzz(uint256 newSqrtQ) public {
-        newSqrtQ = bound(newSqrtQ, 1.4e18, 1_000_000e18);
+    function testWithDifferentPriceRange_Fuzz(uint256 newSqrtQ0) public {
+        newSqrtQ0 = bound(newSqrtQ0, 1.001e18, 1_000_000e18); // Price range cannot be lower than 1.
 
-        uint256 initialSqrtQ = sqrtQ0();
-        setSqrtQ0(newSqrtQ);
+        uint256 initialSqrtQ0 = sqrtQ0();
+        setSqrtQ0(newSqrtQ0);
         (address firstPool, address secondPool) = _createNewPool();
 
         uint256[] memory curentFirstPoolVirtualBalances = AclAmmPool(firstPool).getLastVirtualBalances();
         uint256[] memory curentNewPoolVirtualBalances = AclAmmPool(secondPool).getLastVirtualBalances();
 
-        if (newSqrtQ > initialSqrtQ) {
+        if (newSqrtQ0 > initialSqrtQ0) {
             assertLt(
                 curentNewPoolVirtualBalances[0],
                 curentFirstPoolVirtualBalances[0],
@@ -171,21 +168,38 @@ contract AclAmmPoolVirtualBalancesTest is BaseAclAmmTest {
         }
     }
 
-    function testSwap_Fuzz(uint256 exactAmountIn) public {
-        exactAmountIn = bound(exactAmountIn, 1e18, 10_000e18);
+    function testSwapExactIn_Fuzz(uint256 exactAmountIn) public {
+        exactAmountIn = bound(exactAmountIn, 1e6, _INITIAL_BALANCE_A);
 
-        uint256[] memory virtualBalances = _calculateVirtualBalances();
+        uint256[] memory oldVirtualBalances = AclAmmPool(pool).getLastVirtualBalances();
         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");
+        assertLe(invariantBefore, invariantAfter, "Invariant should not decrease");
 
-        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");
+        uint256[] memory newVirtualBalances = AclAmmPool(pool).getLastVirtualBalances();
+        assertEq(newVirtualBalances[0], oldVirtualBalances[0], "Virtual A balances do not match");
+        assertEq(newVirtualBalances[1], oldVirtualBalances[1], "Virtual B balances do not match");
+    }
+
+    function testSwapExactOut_Fuzz(uint256 exactAmountOut) public {
+        exactAmountOut = bound(exactAmountOut, 1e6, _INITIAL_BALANCE_B);
+
+        uint256[] memory virtualBalances = _calculateVirtualBalances();
+        uint256 invariantBefore = _getCurrentInvariant();
+
+        vm.prank(alice);
+        router.swapSingleTokenExactOut(pool, dai, usdc, exactAmountOut, UINT256_MAX, UINT256_MAX, false, new bytes(0));
+
+        uint256 invariantAfter = _getCurrentInvariant();
+        assertLe(invariantBefore, invariantAfter, "Invariant should not decrease");
+
+        uint256[] memory currentVirtualBalances = AclAmmPool(pool).getLastVirtualBalances();
+        assertEq(currentVirtualBalances[0], virtualBalances[0], "Virtual A balances don't equal");
+        assertEq(currentVirtualBalances[1], virtualBalances[1], "Virtual B balances don't equal");
     }
 
     function testAddLiquidity_Fuzz(uint256 exactBptAmountOut) public {
@@ -238,8 +252,8 @@ contract AclAmmPoolVirtualBalancesTest is BaseAclAmmTest {
         virtualBalances = new uint256[](2);
 
         uint256 sqrtQMinusOne = sqrtQ0() - FixedPoint.ONE;
-        virtualBalances[0] = initialABalance.divDown(sqrtQMinusOne);
-        virtualBalances[1] = initialBBalance.divDown(sqrtQMinusOne);
+        virtualBalances[0] = _INITIAL_BALANCE_A.divDown(sqrtQMinusOne);
+        virtualBalances[1] = _INITIAL_BALANCE_B.divDown(sqrtQMinusOne);
     }
 
     function _createNewPool() internal returns (address initalPool, address newPool) {
diff --git a/test/foundry/utils/BaseAclAmmTest.sol b/test/foundry/utils/BaseAclAmmTest.sol
index fb5e4d1..1fc30d5 100644
--- a/test/foundry/utils/BaseAclAmmTest.sol
+++ b/test/foundry/utils/BaseAclAmmTest.sol
@@ -57,14 +57,13 @@ contract BaseAclAmmTest is AclAmmPoolContractsDeployer, BaseVaultTest {
         (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);
+    function setPriceRange(uint256 priceRange) internal {
+        uint256 Q0 = GyroPoolMath.sqrt(priceRange, 5);
         _sqrtQ0 = GyroPoolMath.sqrt(Q0, 5);
     }
 
-    function setSqrtQ0(uint256 sqrtQ0_) internal {
-        _sqrtQ0 = sqrtQ0_;
+    function setSqrtQ0(uint256 newSqrtQ0) internal {
+        _sqrtQ0 = newSqrtQ0;
     }
 
     function sqrtQ0() internal view returns (uint256) {