@@ -17,7 +17,9 @@ import {ICoin} from "../src/interfaces/ICoin.sol";
1717import {Currency} from "@uniswap/v4-core/src/types/Currency.sol " ;
1818import {CoinCommon} from "../src/libs/CoinCommon.sol " ;
1919import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol " ;
20+ import {MultiOwnable} from "../src/utils/MultiOwnable.sol " ;
2021import {IHooksUpgradeGate} from "../src/interfaces/IHooksUpgradeGate.sol " ;
22+ import {BaseCoin} from "../src/BaseCoin.sol " ;
2123
2224contract LiquidityMigrationReceiver is IUpgradeableDestinationV4Hook , IERC165 {
2325 function initializeFromMigration (
@@ -45,6 +47,8 @@ contract InvalidLiquidityMigrationReceiver is IERC165 {
4547contract 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