@zoralabs/coins 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build$colon$js.log +125 -106
- package/CHANGELOG.md +50 -5
- package/README.md +5 -0
- package/abis/AddressConstants.json +7 -0
- package/abis/BaseCoin.json +0 -5
- package/abis/BaseTest.json +62 -0
- package/abis/BuySupplyWithV4SwapHook.json +429 -0
- package/abis/ContentCoin.json +0 -5
- package/abis/CreatorCoin.json +0 -5
- package/abis/FeeEstimatorHook.json +94 -1
- package/abis/IUniswapV4Router04.json +484 -0
- package/abis/IUpgradeableDestinationV4HookWithUpdateableFee.json +95 -0
- package/abis/IZoraFactory.json +69 -0
- package/abis/MockAirlock.json +39 -0
- package/abis/SimpleERC20.json +326 -0
- package/abis/ZoraFactoryImpl.json +69 -0
- package/abis/ZoraV4CoinHook.json +94 -1
- package/addresses/8453.json +8 -10
- package/audits/report-cantinacode-zora-0827.pdf +3498 -4
- package/audits/report-cantinacode-zora-1021.pdf +0 -0
- package/dist/index.cjs +161 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +160 -21
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +259 -40
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/foundry.toml +3 -3
- package/package/wagmiGenerated.ts +160 -21
- package/package.json +1 -1
- package/script/DeployPostDeploymentHooks.s.sol +1 -3
- package/script/TestBackingCoinSwap.s.sol +0 -2
- package/script/TestV4Swap.s.sol +0 -2
- package/src/BaseCoin.sol +4 -12
- package/src/ContentCoin.sol +3 -4
- package/src/CreatorCoin.sol +8 -10
- package/src/ZoraFactoryImpl.sol +115 -83
- package/src/deployment/CoinsDeployerBase.sol +9 -8
- package/src/hook-registry/ZoraHookRegistry.sol +4 -0
- package/src/hooks/ZoraV4CoinHook.sol +66 -9
- package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +310 -0
- package/src/interfaces/IUpgradeableV4Hook.sol +18 -0
- package/src/interfaces/IZoraFactory.sol +21 -2
- package/src/libs/CoinConstants.sol +51 -8
- package/src/libs/CoinDopplerMultiCurve.sol +11 -11
- package/src/libs/CoinRewardsV4.sol +26 -33
- package/src/libs/CoinSetup.sol +2 -9
- package/src/libs/DopplerMath.sol +2 -2
- package/src/libs/V4Liquidity.sol +79 -15
- package/src/utils/AutoSwapper.sol +1 -1
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/BuySupplyWithV4SwapHook.t.sol +509 -0
- package/test/Coin.t.sol +26 -14
- package/test/CoinRewardsV4.t.sol +33 -0
- package/test/CoinUniV4.t.sol +3 -5
- package/test/ContentCoinRewards.t.sol +44 -3
- package/test/CreatorCoin.t.sol +54 -33
- package/test/CreatorCoinRewards.t.sol +1 -3
- package/test/DeploymentHooks.t.sol +54 -2
- package/test/Factory.t.sol +3 -3
- package/test/LiquidityMigration.t.sol +145 -7
- package/test/MultiOwnable.t.sol +4 -4
- package/test/Upgrades.t.sol +26 -17
- package/test/V4Liquidity.t.sol +178 -0
- package/test/ZoraHookRegistry.t.sol +19 -9
- package/test/mocks/MockAirlock.sol +22 -0
- package/test/mocks/SimpleERC20.sol +8 -0
- package/test/utils/BaseTest.sol +155 -3
- package/test/utils/RewardTestHelpers.sol +4 -4
- package/test/utils/hookmate/README.md +50 -0
- package/test/utils/hookmate/artifacts/DeployHelper.sol +20 -0
- package/test/utils/hookmate/artifacts/Permit2.sol +16 -0
- package/test/utils/hookmate/artifacts/UniversalRouter.sol +29 -0
- package/test/utils/hookmate/artifacts/V4PoolManager.sol +17 -0
- package/test/utils/hookmate/artifacts/V4PositionManager.sol +23 -0
- package/test/utils/hookmate/artifacts/V4Quoter.sol +17 -0
- package/test/utils/hookmate/artifacts/V4Router.sol +18 -0
- package/test/utils/hookmate/constants/AddressConstants.sol +193 -0
- package/test/utils/hookmate/interfaces/router/IUniswapV4Router04.sol +173 -0
- package/test/utils/hookmate/interfaces/router/PathKey.sol +34 -0
- package/test/utils/hookmate/test/utils/SwapFeeEventAsserter.sol +24 -0
- package/wagmi.config.ts +1 -1
- package/abis/CoinConstants.json +0 -54
- package/abis/CoinRewardsV4.json +0 -67
- package/src/libs/CreatorCoinConstants.sol +0 -15
- package/src/libs/MarketConstants.sol +0 -23
- package/src/utils/uniswap/BytesLib.sol +0 -35
- package/src/utils/uniswap/Path.sol +0 -31
- /package/abis/{VmContractHelper227.json → VmContractHelper239.json} +0 -0
|
@@ -17,7 +17,13 @@ import {ICoin} from "../src/interfaces/ICoin.sol";
|
|
|
17
17
|
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
18
18
|
import {CoinCommon} from "../src/libs/CoinCommon.sol";
|
|
19
19
|
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
20
|
+
import {MultiOwnable} from "../src/utils/MultiOwnable.sol";
|
|
20
21
|
import {IHooksUpgradeGate} from "../src/interfaces/IHooksUpgradeGate.sol";
|
|
22
|
+
import {BaseCoin} from "../src/BaseCoin.sol";
|
|
23
|
+
import {CoinConstants} from "../src/libs/CoinConstants.sol";
|
|
24
|
+
import {SwapParams} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
25
|
+
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
26
|
+
import {BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
21
27
|
|
|
22
28
|
contract LiquidityMigrationReceiver is IUpgradeableDestinationV4Hook, IERC165 {
|
|
23
29
|
function initializeFromMigration(
|
|
@@ -45,6 +51,8 @@ contract InvalidLiquidityMigrationReceiver is IERC165 {
|
|
|
45
51
|
contract LiquidityMigrationTest is BaseTest {
|
|
46
52
|
MockERC20 internal mockERC20A;
|
|
47
53
|
|
|
54
|
+
address constant coinVersionLookup = 0x777777751622c0d3258f214F9DF38E35BF45baF3;
|
|
55
|
+
|
|
48
56
|
function setUp() public override {
|
|
49
57
|
super.setUpWithBlockNumber(30267794);
|
|
50
58
|
|
|
@@ -125,31 +133,59 @@ contract LiquidityMigrationTest is BaseTest {
|
|
|
125
133
|
assertEq(newPoolKey.tickSpacing, poolKey.tickSpacing, "poolkey tickSpacing");
|
|
126
134
|
}
|
|
127
135
|
|
|
128
|
-
function
|
|
136
|
+
function test_migrateLiquidity_revertsSwapsOnOldPoolKey() public {
|
|
129
137
|
address currency = address(mockERC20A);
|
|
130
138
|
mockERC20A.mint(address(poolManager), 1_000_000_000 ether);
|
|
131
139
|
_deployV4Coin(currency);
|
|
132
140
|
|
|
133
141
|
address trader = makeAddr("trader");
|
|
134
|
-
|
|
135
142
|
mockERC20A.mint(trader, 10 ether);
|
|
136
143
|
|
|
137
|
-
// do some swaps
|
|
144
|
+
// do some swaps before migration
|
|
138
145
|
_swapSomeCurrencyForCoin(coinV4, currency, 1 ether, trader);
|
|
139
146
|
_swapSomeCoinForCurrency(coinV4, currency, uint128(coinV4.balanceOf(trader)), trader);
|
|
140
147
|
|
|
141
|
-
address newHook = address(new LiquidityMigrationReceiver());
|
|
142
|
-
|
|
143
148
|
PoolKey memory poolKey = coinV4.getPoolKey();
|
|
149
|
+
bytes32 poolKeyHash = CoinCommon.hashPoolKey(poolKey);
|
|
150
|
+
|
|
151
|
+
// Verify the mapping exists before migration
|
|
152
|
+
IZoraV4CoinHook.PoolCoin memory poolCoinBefore = hook.getPoolCoin(poolKey);
|
|
153
|
+
assertEq(poolCoinBefore.coin, address(coinV4), "pool coin should exist before migration");
|
|
154
|
+
|
|
155
|
+
IZoraV4CoinHook.PoolCoin memory poolCoinByHashBefore = hook.getPoolCoinByHash(poolKeyHash);
|
|
156
|
+
assertEq(poolCoinByHashBefore.coin, address(coinV4), "pool coin by hash should exist before migration");
|
|
144
157
|
|
|
158
|
+
address newHook = address(new LiquidityMigrationReceiver());
|
|
145
159
|
registerUpgradePath(address(poolKey.hooks), address(newHook));
|
|
146
160
|
|
|
147
161
|
// migrate the liquidity
|
|
148
162
|
vm.prank(users.creator);
|
|
149
163
|
coinV4.migrateLiquidity(address(newHook), "");
|
|
150
164
|
|
|
151
|
-
//
|
|
152
|
-
|
|
165
|
+
// Verify that the old pool key mapping has been deleted
|
|
166
|
+
IZoraV4CoinHook.PoolCoin memory poolCoinAfter = hook.getPoolCoin(poolKey);
|
|
167
|
+
assertEq(poolCoinAfter.coin, address(0), "old pool key should have no associated coin after migration");
|
|
168
|
+
assertEq(poolCoinAfter.positions.length, 0, "old pool coin positions should be empty after migration");
|
|
169
|
+
|
|
170
|
+
IZoraV4CoinHook.PoolCoin memory poolCoinByHashAfter = hook.getPoolCoinByHash(poolKeyHash);
|
|
171
|
+
assertEq(poolCoinByHashAfter.coin, address(0), "old pool coin by hash should have no associated coin after migration");
|
|
172
|
+
assertEq(poolCoinByHashAfter.positions.length, 0, "old pool coin by hash positions should be empty after migration");
|
|
173
|
+
|
|
174
|
+
// Verify that hook operations revert with NoCoinForHook on the old poolkey
|
|
175
|
+
vm.expectRevert(abi.encodeWithSelector(IZoraV4CoinHook.NoCoinForHook.selector, poolKey));
|
|
176
|
+
|
|
177
|
+
SwapParams memory mockSwapParams = SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: 0});
|
|
178
|
+
|
|
179
|
+
BalanceDelta mockDelta = BalanceDeltaLibrary.ZERO_DELTA;
|
|
180
|
+
|
|
181
|
+
// Call afterSwap directly - this should revert with NoCoinForHook
|
|
182
|
+
vm.prank(address(poolManager));
|
|
183
|
+
IHooks(address(hook)).afterSwap(trader, poolKey, mockSwapParams, mockDelta, "");
|
|
184
|
+
|
|
185
|
+
// Verify the new pool key still works
|
|
186
|
+
PoolKey memory newPoolKey = coinV4.getPoolKey();
|
|
187
|
+
assertEq(address(newPoolKey.hooks), address(newHook), "coin should have updated to new hook");
|
|
188
|
+
assertTrue(address(poolKey.hooks) != address(newPoolKey.hooks), "old and new pool keys should be different");
|
|
153
189
|
}
|
|
154
190
|
|
|
155
191
|
function test_migrateLiquidity_emitsLiquidityMigrated() public {
|
|
@@ -383,4 +419,106 @@ contract LiquidityMigrationTest is BaseTest {
|
|
|
383
419
|
// Should match isRegisteredUpgradePath
|
|
384
420
|
assertEq(hookUpgradeGate.isAllowedHookUpgrade(baseImpl, upgradeImpl), hookUpgradeGate.isRegisteredUpgradePath(baseImpl, upgradeImpl));
|
|
385
421
|
}
|
|
422
|
+
|
|
423
|
+
function test_migrateLiquidity_failsWithEmptyPositionBug() public {
|
|
424
|
+
// Reproduce the bug discovered in hook version 1.1.2 where migration
|
|
425
|
+
// tries to modify liquidity positions that have zero liquidity
|
|
426
|
+
vm.createSelectFork("base", 35671635);
|
|
427
|
+
|
|
428
|
+
address contentCoin = 0x81f5F30217dA777a5d6441606AFa57E093833d7C;
|
|
429
|
+
address oldHook = 0x9ea932730A7787000042e34390B8E435dD839040; // v1.1.2 hook
|
|
430
|
+
address newHook = 0xff74Be9D3596eA7a33BB4983DD7906fB34135040; // current hook
|
|
431
|
+
address upgradeGate = 0xD88f6BdD765313CaFA5888C177c325E2C3AbF2D2; // deployed upgrade gate
|
|
432
|
+
|
|
433
|
+
BaseCoin coin = BaseCoin(contentCoin);
|
|
434
|
+
|
|
435
|
+
uint24 oldFee = coin.getPoolKey().fee;
|
|
436
|
+
|
|
437
|
+
// Register upgrade path
|
|
438
|
+
address[] memory baseImpls = new address[](1);
|
|
439
|
+
baseImpls[0] = oldHook;
|
|
440
|
+
|
|
441
|
+
vm.prank(Ownable(upgradeGate).owner());
|
|
442
|
+
IHooksUpgradeGate(upgradeGate).registerUpgradePath(baseImpls, newHook);
|
|
443
|
+
|
|
444
|
+
// Get coin owner
|
|
445
|
+
address coinOwner = MultiOwnable(contentCoin).owners()[0];
|
|
446
|
+
|
|
447
|
+
// First, demonstrate the bug exists - this should fail
|
|
448
|
+
vm.prank(coinOwner);
|
|
449
|
+
vm.expectRevert();
|
|
450
|
+
coin.migrateLiquidity(newHook, "");
|
|
451
|
+
|
|
452
|
+
// Now fix the bug by etching fixed hook code onto the old hook address
|
|
453
|
+
bytes memory creationCode = HooksDeployment.makeHookCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate);
|
|
454
|
+
|
|
455
|
+
(IHooks fixedHook, ) = HooksDeployment.deployHookWithExistingOrNewSalt(address(this), creationCode, bytes32(0));
|
|
456
|
+
|
|
457
|
+
// Etch the fixed hook code onto the old hook address
|
|
458
|
+
vm.etch(oldHook, address(fixedHook).code);
|
|
459
|
+
|
|
460
|
+
// Now migration should work
|
|
461
|
+
vm.prank(coinOwner);
|
|
462
|
+
coin.migrateLiquidity(newHook, "");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function test_migrateLiquidity_canUseNewFee() public {
|
|
466
|
+
// Reproduce the bug discovered in hook version 1.1.2 where migration
|
|
467
|
+
// tries to modify liquidity positions that have zero liquidity
|
|
468
|
+
vm.createSelectFork("base", 35754730);
|
|
469
|
+
|
|
470
|
+
// jacob creator coin
|
|
471
|
+
BaseCoin coin = BaseCoin(0x9B13358E3a023507E7046c18f508A958cDA75f54);
|
|
472
|
+
|
|
473
|
+
address upgradeGate = 0xD88f6BdD765313CaFA5888C177c325E2C3AbF2D2; // live upgrade gate
|
|
474
|
+
|
|
475
|
+
uint24 oldFee = coin.getPoolKey().fee;
|
|
476
|
+
|
|
477
|
+
assertEq(oldFee, 30000);
|
|
478
|
+
|
|
479
|
+
// Now fix the bug by etching fixed hook code onto the old hook address
|
|
480
|
+
bytes memory creationCode = HooksDeployment.makeHookCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate);
|
|
481
|
+
|
|
482
|
+
(IHooks newHook, ) = HooksDeployment.deployHookWithExistingOrNewSalt(address(this), creationCode, bytes32(0));
|
|
483
|
+
|
|
484
|
+
// Register upgrade path
|
|
485
|
+
address[] memory baseImpls = new address[](1);
|
|
486
|
+
baseImpls[0] = address(coin.hooks());
|
|
487
|
+
|
|
488
|
+
vm.prank(Ownable(upgradeGate).owner());
|
|
489
|
+
IHooksUpgradeGate(upgradeGate).registerUpgradePath(baseImpls, address(newHook));
|
|
490
|
+
|
|
491
|
+
// Get coin owner
|
|
492
|
+
address coinOwner = MultiOwnable(address(coin)).owners()[0];
|
|
493
|
+
|
|
494
|
+
vm.prank(coinOwner);
|
|
495
|
+
coin.migrateLiquidity(address(newHook), "");
|
|
496
|
+
|
|
497
|
+
// fee should still be the same as before, because we didnt have the logic to update the fee in the old coin's hook.
|
|
498
|
+
assertEq(coin.getPoolKey().fee, oldFee);
|
|
499
|
+
|
|
500
|
+
address currencyAddress = address(coin.currency());
|
|
501
|
+
|
|
502
|
+
// now test swapping the migrated liquidity
|
|
503
|
+
address trader = makeAddr("trader");
|
|
504
|
+
deal(currencyAddress, trader, 10 ether);
|
|
505
|
+
_swapSomeCurrencyForCoin(coin, coin.currency(), 1 ether, trader);
|
|
506
|
+
|
|
507
|
+
// now migrate liquidity again, but this time to the same new hook as before
|
|
508
|
+
// since the bug has been fixed in the new hook, we should now be able to get the new fee
|
|
509
|
+
// register the upgrade path for the new hook to itself
|
|
510
|
+
baseImpls[0] = address(newHook);
|
|
511
|
+
vm.prank(Ownable(upgradeGate).owner());
|
|
512
|
+
IHooksUpgradeGate(upgradeGate).registerUpgradePath(baseImpls, address(newHook));
|
|
513
|
+
|
|
514
|
+
// migrate liquidity again to the same new hook as before
|
|
515
|
+
vm.prank(coinOwner);
|
|
516
|
+
coin.migrateLiquidity(address(newHook), "");
|
|
517
|
+
|
|
518
|
+
// the new fee should be the correct current fee
|
|
519
|
+
assertEq(coin.getPoolKey().fee, CoinConstants.LP_FEE_V4);
|
|
520
|
+
|
|
521
|
+
// now test swapping the migrated liquidity - it should work
|
|
522
|
+
_swapSomeCurrencyForCoin(coin, coin.currency(), 1 ether, trader);
|
|
523
|
+
}
|
|
386
524
|
}
|
package/test/MultiOwnable.t.sol
CHANGED
|
@@ -5,7 +5,7 @@ import "./utils/BaseTest.sol";
|
|
|
5
5
|
|
|
6
6
|
contract MultiOwnableTest is BaseTest {
|
|
7
7
|
function setUp() public override {
|
|
8
|
-
super.
|
|
8
|
+
super.setUpNonForked();
|
|
9
9
|
|
|
10
10
|
_deployV4Coin();
|
|
11
11
|
}
|
|
@@ -135,7 +135,7 @@ contract MultiOwnableTest is BaseTest {
|
|
|
135
135
|
|
|
136
136
|
function test_revert_init_with_zero_owners() public {
|
|
137
137
|
address[] memory emptyOwners = new address[](0);
|
|
138
|
-
bytes memory poolConfig_ = _generatePoolConfig(address(
|
|
138
|
+
bytes memory poolConfig_ = _generatePoolConfig(address(0));
|
|
139
139
|
vm.expectRevert(MultiOwnable.OneOwnerRequired.selector);
|
|
140
140
|
factory.deploy(users.creator, emptyOwners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
|
|
141
141
|
}
|
|
@@ -143,7 +143,7 @@ contract MultiOwnableTest is BaseTest {
|
|
|
143
143
|
function test_revert_init_with_zero_address() public {
|
|
144
144
|
address[] memory owners = new address[](1);
|
|
145
145
|
owners[0] = address(0);
|
|
146
|
-
bytes memory poolConfig_ = _generatePoolConfig(address(
|
|
146
|
+
bytes memory poolConfig_ = _generatePoolConfig(address(0));
|
|
147
147
|
vm.expectRevert(MultiOwnable.OwnerCannotBeAddressZero.selector);
|
|
148
148
|
factory.deploy(users.creator, owners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
|
|
149
149
|
}
|
|
@@ -152,7 +152,7 @@ contract MultiOwnableTest is BaseTest {
|
|
|
152
152
|
address[] memory owners = new address[](2);
|
|
153
153
|
owners[0] = users.creator;
|
|
154
154
|
owners[1] = users.creator;
|
|
155
|
-
bytes memory poolConfig_ = _generatePoolConfig(address(
|
|
155
|
+
bytes memory poolConfig_ = _generatePoolConfig(address(0));
|
|
156
156
|
vm.expectRevert(MultiOwnable.AlreadyOwner.selector);
|
|
157
157
|
factory.deploy(users.creator, owners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
|
|
158
158
|
}
|
package/test/Upgrades.t.sol
CHANGED
|
@@ -14,6 +14,7 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
|
14
14
|
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
15
15
|
import {BuySupplyWithSwapRouterHook} from "../src/hooks/deployment/BuySupplyWithSwapRouterHook.sol";
|
|
16
16
|
import {ZoraV4CoinHook} from "../src/hooks/ZoraV4CoinHook.sol";
|
|
17
|
+
import {BuySupplyWithV4SwapHook} from "../src/hooks/deployment/BuySupplyWithV4SwapHook.sol";
|
|
17
18
|
import {console} from "forge-std/console.sol";
|
|
18
19
|
import {IDeployedCoinVersionLookup} from "../src/interfaces/IDeployedCoinVersionLookup.sol";
|
|
19
20
|
import {IHooksUpgradeGate} from "../src/interfaces/IHooksUpgradeGate.sol";
|
|
@@ -106,26 +107,34 @@ contract UpgradesTest is BaseTest, CoinsDeployerBase {
|
|
|
106
107
|
|
|
107
108
|
uint128 amountIn = 1 ether;
|
|
108
109
|
|
|
109
|
-
address
|
|
110
|
-
|
|
111
|
-
// build weth to usdc swap
|
|
112
|
-
bytes memory call = abi.encodeWithSelector(
|
|
113
|
-
ISwapRouter.exactInputSingle.selector,
|
|
114
|
-
ISwapRouter.ExactInputSingleParams({
|
|
115
|
-
tokenIn: WETH_ADDRESS,
|
|
116
|
-
tokenOut: ZORA,
|
|
117
|
-
fee: 3000,
|
|
118
|
-
recipient: buySupplyWithSwapRouterHook,
|
|
119
|
-
amountIn: amountIn,
|
|
120
|
-
amountOutMinimum: 0,
|
|
121
|
-
sqrtPriceLimitX96: 0
|
|
122
|
-
})
|
|
123
|
-
);
|
|
110
|
+
address buySupplyWithV4SwapHook = deployment.buySupplyWithSwapRouterHook;
|
|
124
111
|
|
|
125
112
|
address buyRecipient = makeAddr("buyRecipient");
|
|
126
113
|
|
|
127
114
|
address trader = 0xC077e4cC02fa01A5b7fAca1acE9BBe9f5ac5Af9F;
|
|
128
115
|
|
|
116
|
+
// Create V3 path: ETH -> ZORA (using exact path from test)
|
|
117
|
+
bytes memory v3Route = abi.encodePacked(
|
|
118
|
+
WETH_ADDRESS, // WETH
|
|
119
|
+
uint24(3000), // fee
|
|
120
|
+
ZORA // ZORA
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// No V4 route needed since coin is backed by ZORA
|
|
124
|
+
PoolKey[] memory v4Route = new PoolKey[](0);
|
|
125
|
+
|
|
126
|
+
// Encode proper BuySupplyWithV4SwapHook data
|
|
127
|
+
BuySupplyWithV4SwapHook.InitialSupplyParams memory params = BuySupplyWithV4SwapHook.InitialSupplyParams({
|
|
128
|
+
buyRecipient: buyRecipient,
|
|
129
|
+
v3Route: v3Route,
|
|
130
|
+
v4Route: v4Route,
|
|
131
|
+
inputCurrency: address(0), // ETH
|
|
132
|
+
inputAmount: amountIn,
|
|
133
|
+
minAmountOut: 0
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
bytes memory hookData = abi.encode(params);
|
|
137
|
+
|
|
129
138
|
vm.startPrank(trader);
|
|
130
139
|
vm.deal(trader, amountIn);
|
|
131
140
|
|
|
@@ -137,8 +146,8 @@ contract UpgradesTest is BaseTest, CoinsDeployerBase {
|
|
|
137
146
|
"TEST",
|
|
138
147
|
poolConfig,
|
|
139
148
|
users.platformReferrer,
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
buySupplyWithV4SwapHook,
|
|
150
|
+
hookData,
|
|
142
151
|
keccak256("test")
|
|
143
152
|
);
|
|
144
153
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import "./utils/BaseTest.sol";
|
|
5
|
+
import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
|
|
6
|
+
import {CoinDopplerMultiCurve} from "../src/libs/CoinDopplerMultiCurve.sol";
|
|
7
|
+
import {V4Liquidity} from "../src/libs/V4Liquidity.sol";
|
|
8
|
+
import {LpPosition} from "../src/types/LpPosition.sol";
|
|
9
|
+
import {PoolConfiguration} from "../src/interfaces/ICoin.sol";
|
|
10
|
+
import {CoinConstants} from "../src/libs/CoinConstants.sol";
|
|
11
|
+
import {MockERC20} from "./mocks/MockERC20.sol";
|
|
12
|
+
import {ContentCoin} from "../src/ContentCoin.sol";
|
|
13
|
+
import {ZoraV4CoinHook} from "../src/hooks/ZoraV4CoinHook.sol";
|
|
14
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
15
|
+
|
|
16
|
+
contract V4LiquidityTest is BaseTest {
|
|
17
|
+
MockERC20 internal mockERC20A;
|
|
18
|
+
|
|
19
|
+
function setUp() public override {
|
|
20
|
+
super.setUp();
|
|
21
|
+
mockERC20A = new MockERC20("MockERC20A", "MCKA");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _poolConfigWithDuplicatePositions(address currency) private pure returns (bytes memory poolConfig) {
|
|
25
|
+
// Create configuration that will produce duplicate positions
|
|
26
|
+
int24[] memory tickLower_ = new int24[](2);
|
|
27
|
+
tickLower_[0] = -54000;
|
|
28
|
+
tickLower_[1] = -54000; // Same as first curve
|
|
29
|
+
|
|
30
|
+
int24[] memory tickUpper_ = new int24[](2);
|
|
31
|
+
tickUpper_[0] = 7000;
|
|
32
|
+
tickUpper_[1] = 7000; // Same as first curve
|
|
33
|
+
|
|
34
|
+
uint16[] memory numDiscoveryPositions_ = new uint16[](2);
|
|
35
|
+
numDiscoveryPositions_[0] = 5;
|
|
36
|
+
numDiscoveryPositions_[1] = 5;
|
|
37
|
+
|
|
38
|
+
uint256[] memory maxDiscoverySupplyShare_ = new uint256[](2);
|
|
39
|
+
maxDiscoverySupplyShare_[0] = 100000000000000000; // 0.1e18
|
|
40
|
+
maxDiscoverySupplyShare_[1] = 100000000000000000; // 0.1e18
|
|
41
|
+
|
|
42
|
+
poolConfig = CoinConfigurationVersions.encodeDopplerMultiCurveUniV4(currency, tickLower_, tickUpper_, numDiscoveryPositions_, maxDiscoverySupplyShare_);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _countDuplicatePositions(LpPosition[] memory positions) private pure returns (uint256 duplicateCount) {
|
|
46
|
+
for (uint256 i = 0; i < positions.length; i++) {
|
|
47
|
+
for (uint256 j = i + 1; j < positions.length; j++) {
|
|
48
|
+
if (positions[i].tickLower == positions[j].tickLower && positions[i].tickUpper == positions[j].tickUpper) {
|
|
49
|
+
duplicateCount++;
|
|
50
|
+
break; // Only count each unique duplicate once
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function test_calculatePositionsWithDuplicateConfigCreatesDuplicates() public view {
|
|
57
|
+
address currency = address(mockERC20A);
|
|
58
|
+
bytes memory poolConfig = _poolConfigWithDuplicatePositions(currency);
|
|
59
|
+
|
|
60
|
+
(, PoolConfiguration memory poolConfiguration) = CoinDopplerMultiCurve.setupPool(true, poolConfig);
|
|
61
|
+
|
|
62
|
+
LpPosition[] memory positions = CoinDopplerMultiCurve.calculatePositions(
|
|
63
|
+
true, // isCoinToken0
|
|
64
|
+
poolConfiguration,
|
|
65
|
+
CoinConstants.CONTENT_COIN_MARKET_SUPPLY
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
uint256 duplicateCount = _countDuplicatePositions(positions);
|
|
69
|
+
assertGt(duplicateCount, 0, "Should have duplicate positions");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function test_dedupePositionsMergesDuplicates() public view {
|
|
73
|
+
address currency = address(mockERC20A);
|
|
74
|
+
bytes memory poolConfig = _poolConfigWithDuplicatePositions(currency);
|
|
75
|
+
|
|
76
|
+
(, PoolConfiguration memory poolConfiguration) = CoinDopplerMultiCurve.setupPool(true, poolConfig);
|
|
77
|
+
|
|
78
|
+
LpPosition[] memory originalPositions = CoinDopplerMultiCurve.calculatePositions(
|
|
79
|
+
true, // isCoinToken0
|
|
80
|
+
poolConfiguration,
|
|
81
|
+
CoinConstants.CONTENT_COIN_MARKET_SUPPLY
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
uint256 originalDuplicateCount = _countDuplicatePositions(originalPositions);
|
|
85
|
+
assertGt(originalDuplicateCount, 0, "Should have duplicate positions to test deduplication");
|
|
86
|
+
|
|
87
|
+
// Deduplicate the positions
|
|
88
|
+
LpPosition[] memory dedupedPositions = V4Liquidity.dedupePositions(originalPositions);
|
|
89
|
+
|
|
90
|
+
// Verify no duplicates exist in deduped array
|
|
91
|
+
uint256 dedupedDuplicateCount = _countDuplicatePositions(dedupedPositions);
|
|
92
|
+
assertEq(dedupedDuplicateCount, 0, "Should have no duplicates after deduplication");
|
|
93
|
+
|
|
94
|
+
// Verify that array is smaller after deduplication
|
|
95
|
+
assertLt(dedupedPositions.length, originalPositions.length, "Deduped array should be smaller");
|
|
96
|
+
|
|
97
|
+
// Calculate total liquidity before and after deduplication
|
|
98
|
+
uint256 totalOriginalLiquidity = 0;
|
|
99
|
+
uint256 totalDedupedLiquidity = 0;
|
|
100
|
+
|
|
101
|
+
for (uint256 i = 0; i < originalPositions.length; i++) {
|
|
102
|
+
totalOriginalLiquidity += originalPositions[i].liquidity;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (uint256 i = 0; i < dedupedPositions.length; i++) {
|
|
106
|
+
totalDedupedLiquidity += dedupedPositions[i].liquidity;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
assertEq(totalOriginalLiquidity, totalDedupedLiquidity, "Total liquidity should be preserved");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function test_memoryStructModification() public pure {
|
|
113
|
+
// this test shows that we can modify a struct in memory and it will be reflected in the array
|
|
114
|
+
LpPosition[] memory positions = new LpPosition[](2);
|
|
115
|
+
positions[0] = LpPosition({tickLower: -100, tickUpper: 100, liquidity: 1000});
|
|
116
|
+
positions[1] = LpPosition({tickLower: -200, tickUpper: 200, liquidity: 2000});
|
|
117
|
+
|
|
118
|
+
LpPosition memory pos = positions[0];
|
|
119
|
+
pos.liquidity += 500;
|
|
120
|
+
pos = positions[1];
|
|
121
|
+
pos.liquidity = 3000;
|
|
122
|
+
|
|
123
|
+
// The array element should be modified
|
|
124
|
+
assertEq(positions[0].liquidity, 1500, "Array element should change when modifying copy");
|
|
125
|
+
assertEq(positions[1].liquidity, 3000, "Array element should change when modifying copy");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function test_mstoreArrayLength() public pure {
|
|
129
|
+
LpPosition[] memory positions = new LpPosition[](5);
|
|
130
|
+
positions[0] = LpPosition({tickLower: -100, tickUpper: 100, liquidity: 1000});
|
|
131
|
+
positions[1] = LpPosition({tickLower: -200, tickUpper: 200, liquidity: 2000});
|
|
132
|
+
positions[2] = LpPosition({tickLower: -300, tickUpper: 300, liquidity: 3000});
|
|
133
|
+
positions[3] = LpPosition({tickLower: -400, tickUpper: 400, liquidity: 4000});
|
|
134
|
+
positions[4] = LpPosition({tickLower: -500, tickUpper: 500, liquidity: 5000});
|
|
135
|
+
|
|
136
|
+
assertEq(positions.length, 5, "Initial length should be 5");
|
|
137
|
+
|
|
138
|
+
assembly {
|
|
139
|
+
mstore(positions, 2)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
assertEq(positions.length, 2, "Length should be 2 after mstore");
|
|
143
|
+
assertEq(positions[0].liquidity, 1000, "First element should be preserved");
|
|
144
|
+
assertEq(positions[1].liquidity, 2000, "Second element should be preserved");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function test_deployedCoinWithDuplicateConfigHasNoDuplicatePositions() public {
|
|
148
|
+
address currency = address(mockERC20A);
|
|
149
|
+
|
|
150
|
+
address[] memory owners = new address[](1);
|
|
151
|
+
owners[0] = users.creator;
|
|
152
|
+
|
|
153
|
+
bytes memory poolConfig = _poolConfigWithDuplicatePositions(currency);
|
|
154
|
+
|
|
155
|
+
(address coinAddress, ) = factory.deploy(
|
|
156
|
+
users.creator,
|
|
157
|
+
owners,
|
|
158
|
+
"https://test.com",
|
|
159
|
+
DEFAULT_NAME,
|
|
160
|
+
DEFAULT_SYMBOL,
|
|
161
|
+
poolConfig,
|
|
162
|
+
address(0),
|
|
163
|
+
address(0),
|
|
164
|
+
bytes(""),
|
|
165
|
+
bytes32(0)
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
ContentCoin coinV4 = ContentCoin(payable(coinAddress));
|
|
169
|
+
|
|
170
|
+
// get hooks
|
|
171
|
+
PoolKey memory poolKey = coinV4.getPoolKey();
|
|
172
|
+
LpPosition[] memory positions = ZoraV4CoinHook(payable(address(coinV4.hooks()))).getPoolCoin(poolKey).positions;
|
|
173
|
+
|
|
174
|
+
// Verify no duplicate positions exist in the deployed coin (deduplication worked during deployment)
|
|
175
|
+
uint256 duplicateCount = _countDuplicatePositions(positions);
|
|
176
|
+
assertEq(duplicateCount, 0, "Should have no duplicates after deployment");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -7,20 +7,28 @@ import {IZoraHookRegistry} from "../src/interfaces/IZoraHookRegistry.sol";
|
|
|
7
7
|
import {ZoraHookRegistry} from "../src/hook-registry/ZoraHookRegistry.sol";
|
|
8
8
|
|
|
9
9
|
contract MockHook {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
string private _version;
|
|
11
|
+
|
|
12
|
+
constructor(string memory version_) {
|
|
13
|
+
_version = version_;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function contractVersion() public view returns (string memory) {
|
|
17
|
+
return _version;
|
|
12
18
|
}
|
|
13
19
|
}
|
|
14
20
|
|
|
21
|
+
contract MockHookNoVersion {
|
|
22
|
+
// No contractVersion function
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
contract ZoraHookRegistryTest is Test {
|
|
16
|
-
uint256 internal forkId;
|
|
17
26
|
address internal owner;
|
|
18
27
|
|
|
19
28
|
ZoraHookRegistry internal zoraHookRegistry;
|
|
20
29
|
MockHook internal mockHook;
|
|
21
30
|
|
|
22
31
|
function setUp() public {
|
|
23
|
-
forkId = vm.createSelectFork("base", 34509280);
|
|
24
32
|
owner = makeAddr("owner");
|
|
25
33
|
|
|
26
34
|
address[] memory initialOwners = new address[](1);
|
|
@@ -29,7 +37,7 @@ contract ZoraHookRegistryTest is Test {
|
|
|
29
37
|
zoraHookRegistry = new ZoraHookRegistry();
|
|
30
38
|
zoraHookRegistry.initialize(initialOwners);
|
|
31
39
|
|
|
32
|
-
mockHook = new MockHook();
|
|
40
|
+
mockHook = new MockHook("0.0.0");
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
function test_register_hooks() public {
|
|
@@ -179,7 +187,8 @@ contract ZoraHookRegistryTest is Test {
|
|
|
179
187
|
address[] memory hooks = new address[](1);
|
|
180
188
|
string[] memory tags = new string[](1);
|
|
181
189
|
|
|
182
|
-
|
|
190
|
+
MockHook hookWithVersion = new MockHook("1.1.1");
|
|
191
|
+
hooks[0] = address(hookWithVersion);
|
|
183
192
|
tags[0] = "CONTENT";
|
|
184
193
|
|
|
185
194
|
vm.prank(owner);
|
|
@@ -192,7 +201,8 @@ contract ZoraHookRegistryTest is Test {
|
|
|
192
201
|
address[] memory hooks = new address[](1);
|
|
193
202
|
string[] memory tags = new string[](1);
|
|
194
203
|
|
|
195
|
-
|
|
204
|
+
MockHookNoVersion hookNoVersion = new MockHookNoVersion();
|
|
205
|
+
hooks[0] = address(hookNoVersion);
|
|
196
206
|
tags[0] = "CONTENT";
|
|
197
207
|
|
|
198
208
|
vm.prank(owner);
|
|
@@ -207,8 +217,8 @@ contract ZoraHookRegistryTest is Test {
|
|
|
207
217
|
|
|
208
218
|
function test_get_hook_addresses_multiple_and_remove_middle() public {
|
|
209
219
|
address a = address(mockHook);
|
|
210
|
-
address b = address(new MockHook());
|
|
211
|
-
address c = address(new MockHook());
|
|
220
|
+
address b = address(new MockHook("0.0.0"));
|
|
221
|
+
address c = address(new MockHook("0.0.0"));
|
|
212
222
|
|
|
213
223
|
address[] memory hooks = new address[](3);
|
|
214
224
|
string[] memory tags = new string[](3);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import {IAirlock} from "../../src/interfaces/IAirlock.sol";
|
|
5
|
+
|
|
6
|
+
/// @title MockAirlock
|
|
7
|
+
/// @notice Mock implementation of IAirlock for testing purposes
|
|
8
|
+
contract MockAirlock is IAirlock {
|
|
9
|
+
address private _owner;
|
|
10
|
+
|
|
11
|
+
constructor(address owner_) {
|
|
12
|
+
_owner = owner_;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function owner() external view override returns (address) {
|
|
16
|
+
return _owner;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function setOwner(address newOwner) external {
|
|
20
|
+
_owner = newOwner;
|
|
21
|
+
}
|
|
22
|
+
}
|