Skip to content

Commit 9f138ad

Browse files
authored
fix: resolve hook migration bug with double liquidity positions
1 parent 44a179b commit 9f138ad

File tree

7 files changed

+74
-7
lines changed

7 files changed

+74
-7
lines changed

docs/pages/changelogs/coins.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Coins Changelog
22

33

4+
## 2.2.1
5+
6+
### Patch Changes
7+
8+
- [c96e0c5e](https://github.com/ourzora/zora-protocol/commit/c96e0c5e): Fix bug where liquidity cannot be migrated if there is a position with 0 liquidity
9+
410
## 2.2.0
511

612
### Minor Changes

packages/coins/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @zoralabs/coins
22

3+
## 2.2.1
4+
5+
### Patch Changes
6+
7+
- c96e0c5e: Fix bug where liquidity cannot be migrated if there is a position with 0 liquidity
8+
39
## 2.2.0
410

511
### Minor Changes

packages/coins/addresses/8453.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
"COIN_V3_IMPL": "0x45Bf86430af7CD071Ea23aE52325A78C8d12aD5a",
44
"COIN_V4_IMPL": "0xca72309AaF706d290E08608b1Af47943902f69b2",
55
"COIN_VERSION": "1.1.0",
6-
"CREATOR_COIN_HOOK": "0x8218FA8d7922e22aED3556a09D5A715F16Ad5040",
7-
"CREATOR_COIN_HOOK_SALT": "0x000000000000000000000000000000000000000000000000000000000000059a",
6+
"CREATOR_COIN_HOOK": "0x1258e5f3C71ca9dCE95Ce734Ba5759532E46D040",
7+
"CREATOR_COIN_HOOK_SALT": "0x00000000000000000000000000000000000000000000000000000000000029b1",
88
"CREATOR_COIN_IMPL": "0x88CC4E08C7608723f3E44e17aC669Fb43b6A8313",
99
"HOOK_UPGRADE_GATE": "0xD88f6BdD765313CaFA5888C177c325E2C3AbF2D2",
1010
"ZORA_FACTORY": "0x777777751622c0d3258f214F9DF38E35BF45baF3",
11-
"ZORA_FACTORY_IMPL": "0x3D419F1Dae4f2E274FB4aEFf4c78DD7Fd02b45F4",
11+
"ZORA_FACTORY_IMPL": "0x57e338b97757f6E416965BEB9A5Cd2DB48b10c42",
1212
"ZORA_HOOK_REGISTRY": "0x777777C4c14b133858c3982D41Dbf02509fc18d7",
13-
"ZORA_V4_COIN_HOOK": "0xff74Be9D3596eA7a33BB4983DD7906fB34135040",
14-
"ZORA_V4_COIN_HOOK_SALT": "0x00000000000000000000000000000000000000000000000000000000000024b9"
13+
"ZORA_V4_COIN_HOOK": "0x2b15a16B3Ef024005bA899Bb51764FCd58Cf9040",
14+
"ZORA_V4_COIN_HOOK_SALT": "0x0000000000000000000000000000000000000000000000000000000000000f90"
1515
}

packages/coins/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zoralabs/coins",
3-
"version": "2.2.0",
3+
"version": "2.2.1",
44
"type": "module",
55
"main": "./dist/index.cjs",
66
"module": "./dist/index.js",

packages/coins/src/libs/V4Liquidity.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,17 @@ library V4Liquidity {
198198
for (uint256 i; i < positions.length; i++) {
199199
uint128 liquidity = getLiquidity(poolManager, address(this), poolKey, positions[i].tickLower, positions[i].tickUpper);
200200

201+
// Skip positions that have no liquidity to avoid CannotUpdateEmptyPosition error
202+
if (liquidity == 0) {
203+
burnedPositions[i] = BurnedPosition({
204+
tickLower: positions[i].tickLower,
205+
tickUpper: positions[i].tickUpper,
206+
amount0Received: 0,
207+
amount1Received: 0
208+
});
209+
continue;
210+
}
211+
201212
ModifyLiquidityParams memory params = ModifyLiquidityParams({
202213
tickLower: positions[i].tickLower,
203214
tickUpper: positions[i].tickUpper,

packages/coins/src/version/ContractVersionBase.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersion
99
contract ContractVersionBase is IVersionedContract {
1010
/// @notice The version of the contract
1111
function contractVersion() external pure override returns (string memory) {
12-
return "2.2.0";
12+
return "2.2.1";
1313
}
1414
}

packages/coins/test/LiquidityMigration.t.sol

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {ICoin} from "../src/interfaces/ICoin.sol";
1717
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
1818
import {CoinCommon} from "../src/libs/CoinCommon.sol";
1919
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
20+
import {MultiOwnable} from "../src/utils/MultiOwnable.sol";
2021
import {IHooksUpgradeGate} from "../src/interfaces/IHooksUpgradeGate.sol";
22+
import {BaseCoin} from "../src/BaseCoin.sol";
2123

2224
contract LiquidityMigrationReceiver is IUpgradeableDestinationV4Hook, IERC165 {
2325
function initializeFromMigration(
@@ -45,6 +47,8 @@ contract InvalidLiquidityMigrationReceiver is IERC165 {
4547
contract LiquidityMigrationTest is BaseTest {
4648
MockERC20 internal mockERC20A;
4749

50+
address constant coinVersionLookup = 0x777777751622c0d3258f214F9DF38E35BF45baF3;
51+
4852
function setUp() public override {
4953
super.setUpWithBlockNumber(30267794);
5054

@@ -383,4 +387,44 @@ contract LiquidityMigrationTest is BaseTest {
383387
// Should match isRegisteredUpgradePath
384388
assertEq(hookUpgradeGate.isAllowedHookUpgrade(baseImpl, upgradeImpl), hookUpgradeGate.isRegisteredUpgradePath(baseImpl, upgradeImpl));
385389
}
390+
391+
function test_migrateLiquidity_failsWithEmptyPositionBug() public {
392+
// Reproduce the bug discovered in hook version 1.1.2 where migration
393+
// tries to modify liquidity positions that have zero liquidity
394+
vm.createSelectFork("base", 35671635);
395+
396+
address contentCoin = 0x81f5F30217dA777a5d6441606AFa57E093833d7C;
397+
address oldHook = 0x9ea932730A7787000042e34390B8E435dD839040; // v1.1.2 hook
398+
address newHook = 0xff74Be9D3596eA7a33BB4983DD7906fB34135040; // current hook
399+
address upgradeGate = 0xD88f6BdD765313CaFA5888C177c325E2C3AbF2D2; // deployed upgrade gate
400+
401+
BaseCoin coin = BaseCoin(contentCoin);
402+
403+
// Register upgrade path
404+
address[] memory baseImpls = new address[](1);
405+
baseImpls[0] = oldHook;
406+
407+
vm.prank(Ownable(upgradeGate).owner());
408+
IHooksUpgradeGate(upgradeGate).registerUpgradePath(baseImpls, newHook);
409+
410+
// Get coin owner
411+
address coinOwner = MultiOwnable(contentCoin).owners()[0];
412+
413+
// First, demonstrate the bug exists - this should fail
414+
vm.prank(coinOwner);
415+
vm.expectRevert();
416+
coin.migrateLiquidity(newHook, "");
417+
418+
// Now fix the bug by etching fixed hook code onto the old hook address
419+
bytes memory creationCode = HooksDeployment.contentCoinCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate);
420+
421+
(IHooks fixedHook, ) = HooksDeployment.deployHookWithExistingOrNewSalt(address(this), creationCode, bytes32(0));
422+
423+
// Etch the fixed hook code onto the old hook address
424+
vm.etch(oldHook, address(fixedHook).code);
425+
426+
// Now migration should work
427+
vm.prank(coinOwner);
428+
coin.migrateLiquidity(newHook, "");
429+
}
386430
}

0 commit comments

Comments
 (0)