From 089baeb988e30b6ef1b381d196922a0b87c9573d Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Thu, 18 Sep 2025 12:33:45 -0600 Subject: [PATCH 1/2] added test that shows the bug, along with a fix --- packages/coins/src/libs/V4Liquidity.sol | 11 +++++ packages/coins/test/LiquidityMigration.t.sol | 44 ++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/packages/coins/src/libs/V4Liquidity.sol b/packages/coins/src/libs/V4Liquidity.sol index 59ae79696..683a083a7 100644 --- a/packages/coins/src/libs/V4Liquidity.sol +++ b/packages/coins/src/libs/V4Liquidity.sol @@ -192,6 +192,17 @@ library V4Liquidity { for (uint256 i; i < positions.length; i++) { uint128 liquidity = getLiquidity(poolManager, address(this), poolKey, positions[i].tickLower, positions[i].tickUpper); + // Skip positions that have no liquidity to avoid CannotUpdateEmptyPosition error + if (liquidity == 0) { + burnedPositions[i] = BurnedPosition({ + tickLower: positions[i].tickLower, + tickUpper: positions[i].tickUpper, + amount0Received: 0, + amount1Received: 0 + }); + continue; + } + ModifyLiquidityParams memory params = ModifyLiquidityParams({ tickLower: positions[i].tickLower, tickUpper: positions[i].tickUpper, diff --git a/packages/coins/test/LiquidityMigration.t.sol b/packages/coins/test/LiquidityMigration.t.sol index de5876691..d86df925d 100644 --- a/packages/coins/test/LiquidityMigration.t.sol +++ b/packages/coins/test/LiquidityMigration.t.sol @@ -17,7 +17,9 @@ import {ICoin} from "../src/interfaces/ICoin.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {CoinCommon} from "../src/libs/CoinCommon.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {MultiOwnable} from "../src/utils/MultiOwnable.sol"; import {IHooksUpgradeGate} from "../src/interfaces/IHooksUpgradeGate.sol"; +import {BaseCoin} from "../src/BaseCoin.sol"; contract LiquidityMigrationReceiver is IUpgradeableDestinationV4Hook, IERC165 { function initializeFromMigration( @@ -45,6 +47,8 @@ contract InvalidLiquidityMigrationReceiver is IERC165 { contract LiquidityMigrationTest is BaseTest { MockERC20 internal mockERC20A; + address constant coinVersionLookup = 0x777777751622c0d3258f214F9DF38E35BF45baF3; + function setUp() public override { super.setUpWithBlockNumber(30267794); @@ -388,4 +392,44 @@ contract LiquidityMigrationTest is BaseTest { // Should match isRegisteredUpgradePath assertEq(hookUpgradeGate.isAllowedHookUpgrade(baseImpl, upgradeImpl), hookUpgradeGate.isRegisteredUpgradePath(baseImpl, upgradeImpl)); } + + function test_migrateLiquidity_failsWithEmptyPositionBug() public { + // Reproduce the bug discovered in hook version 1.1.2 where migration + // tries to modify liquidity positions that have zero liquidity + vm.createSelectFork("base", 35671635); + + address contentCoin = 0x81f5F30217dA777a5d6441606AFa57E093833d7C; + address oldHook = 0x9ea932730A7787000042e34390B8E435dD839040; // v1.1.2 hook + address newHook = 0xff74Be9D3596eA7a33BB4983DD7906fB34135040; // current hook + address upgradeGate = 0xD88f6BdD765313CaFA5888C177c325E2C3AbF2D2; // deployed upgrade gate + + BaseCoin coin = BaseCoin(contentCoin); + + // Register upgrade path + address[] memory baseImpls = new address[](1); + baseImpls[0] = oldHook; + + vm.prank(Ownable(upgradeGate).owner()); + IHooksUpgradeGate(upgradeGate).registerUpgradePath(baseImpls, newHook); + + // Get coin owner + address coinOwner = MultiOwnable(contentCoin).owners()[0]; + + // First, demonstrate the bug exists - this should fail + vm.prank(coinOwner); + vm.expectRevert(); + coin.migrateLiquidity(newHook, ""); + + // Now fix the bug by etching fixed hook code onto the old hook address + bytes memory creationCode = HooksDeployment.contentCoinCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate); + + (IHooks fixedHook, ) = HooksDeployment.deployHookWithExistingOrNewSalt(address(this), creationCode, bytes32(0)); + + // Etch the fixed hook code onto the old hook address + vm.etch(oldHook, address(fixedHook).code); + + // Now migration should work + vm.prank(coinOwner); + coin.migrateLiquidity(newHook, ""); + } } From 33a8966545cb14425e361f4f65dc39a2160d2633 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Thu, 18 Sep 2025 12:33:54 -0600 Subject: [PATCH 2/2] version bump and deploy --- docs/pages/changelogs/coins.mdx | 6 ++++++ packages/coins/CHANGELOG.md | 6 ++++++ packages/coins/addresses/8453.json | 12 ++++++------ packages/coins/package.json | 2 +- packages/coins/src/version/ContractVersionBase.sol | 2 +- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/pages/changelogs/coins.mdx b/docs/pages/changelogs/coins.mdx index e2736f503..ec38cb00f 100644 --- a/docs/pages/changelogs/coins.mdx +++ b/docs/pages/changelogs/coins.mdx @@ -1,6 +1,12 @@ # Coins Changelog +## 2.2.1 + +### Patch Changes + +- [c96e0c5e](https://github.com/ourzora/zora-protocol/commit/c96e0c5e): Fix bug where liquidity cannot be migrated if there is a position with 0 liquidity + ## 2.2.0 ### Minor Changes diff --git a/packages/coins/CHANGELOG.md b/packages/coins/CHANGELOG.md index 982627a5c..ef8df60fe 100644 --- a/packages/coins/CHANGELOG.md +++ b/packages/coins/CHANGELOG.md @@ -1,5 +1,11 @@ # @zoralabs/coins +## 2.2.1 + +### Patch Changes + +- c96e0c5e: Fix bug where liquidity cannot be migrated if there is a position with 0 liquidity + ## 2.2.0 ### Minor Changes diff --git a/packages/coins/addresses/8453.json b/packages/coins/addresses/8453.json index b4b2950e1..0d26ddae0 100644 --- a/packages/coins/addresses/8453.json +++ b/packages/coins/addresses/8453.json @@ -3,13 +3,13 @@ "COIN_V3_IMPL": "0x45Bf86430af7CD071Ea23aE52325A78C8d12aD5a", "COIN_V4_IMPL": "0xca72309AaF706d290E08608b1Af47943902f69b2", "COIN_VERSION": "1.1.0", - "CREATOR_COIN_HOOK": "0xd61A675F8a0c67A73DC3B54FB7318B4D91409040", - "CREATOR_COIN_HOOK_SALT": "0x0000000000000000000000000000000000000000000000000000000000000ae8", + "CREATOR_COIN_HOOK": "0x1258e5f3C71ca9dCE95Ce734Ba5759532E46D040", + "CREATOR_COIN_HOOK_SALT": "0x00000000000000000000000000000000000000000000000000000000000029b1", "CREATOR_COIN_IMPL": "0x88CC4E08C7608723f3E44e17aC669Fb43b6A8313", "HOOK_UPGRADE_GATE": "0xD88f6BdD765313CaFA5888C177c325E2C3AbF2D2", "ZORA_FACTORY": "0x777777751622c0d3258f214F9DF38E35BF45baF3", - "ZORA_FACTORY_IMPL": "0x0e2ea62E5377D46FeF114A60AfBE3d5eA7490577", - "ZORA_V4_COIN_HOOK": "0x9ea932730A7787000042e34390B8E435dD839040", - "ZORA_V4_COIN_HOOK_SALT": "0x0000000000000000000000000000000000000000000000000000000000002fa2", - "ZORA_HOOK_REGISTRY": "0x777777C4c14b133858c3982D41Dbf02509fc18d7" + "ZORA_FACTORY_IMPL": "0x57e338b97757f6E416965BEB9A5Cd2DB48b10c42", + "ZORA_HOOK_REGISTRY": "0x777777C4c14b133858c3982D41Dbf02509fc18d7", + "ZORA_V4_COIN_HOOK": "0x2b15a16B3Ef024005bA899Bb51764FCd58Cf9040", + "ZORA_V4_COIN_HOOK_SALT": "0x0000000000000000000000000000000000000000000000000000000000000f90" } diff --git a/packages/coins/package.json b/packages/coins/package.json index 21a595e3a..a16c94398 100644 --- a/packages/coins/package.json +++ b/packages/coins/package.json @@ -1,6 +1,6 @@ { "name": "@zoralabs/coins", - "version": "2.2.0", + "version": "2.2.1", "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", diff --git a/packages/coins/src/version/ContractVersionBase.sol b/packages/coins/src/version/ContractVersionBase.sol index 4113ad950..24a23b541 100644 --- a/packages/coins/src/version/ContractVersionBase.sol +++ b/packages/coins/src/version/ContractVersionBase.sol @@ -9,6 +9,6 @@ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersion contract ContractVersionBase is IVersionedContract { /// @notice The version of the contract function contractVersion() external pure override returns (string memory) { - return "2.2.0"; + return "2.2.1"; } }