@zoralabs/coins 1.0.0 → 1.0.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.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +123 -75
  2. package/CHANGELOG.md +6 -0
  3. package/abis/BuySupplyWithSwapRouterHook.json +40 -0
  4. package/abis/CoinUniV4Test.json +42 -4
  5. package/abis/FeeEstimatorHook.json +0 -13
  6. package/abis/HooksTest.json +13 -0
  7. package/abis/IZoraFactory.json +19 -0
  8. package/abis/UpgradesTest.json +14 -0
  9. package/abis/ZoraV4CoinHook.json +0 -13
  10. package/addresses/8453.json +7 -8
  11. package/addresses/84532.json +8 -9
  12. package/addresses/dev/8453.json +10 -0
  13. package/dist/index.cjs +15 -1
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.js +15 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/wagmiGenerated.d.ts +30 -0
  18. package/dist/wagmiGenerated.d.ts.map +1 -1
  19. package/foundry.toml +1 -0
  20. package/package/wagmiGenerated.ts +14 -0
  21. package/package.json +1 -1
  22. package/script/Deploy.s.sol +1 -1
  23. package/script/DeployDevFactory.s.sol +2 -2
  24. package/script/DeployHooks.s.sol +1 -1
  25. package/script/PrintUpgradeCommand.s.sol +1 -1
  26. package/script/TestBackingCoinSwap.s.sol +25 -24
  27. package/script/TestV4Swap.s.sol +6 -6
  28. package/script/UpgradeCoinImpl.sol +2 -2
  29. package/script/UpgradeFactoryImpl.s.sol +1 -1
  30. package/src/BaseCoin.sol +14 -0
  31. package/{script → src/deployment}/CoinsDeployerBase.sol +59 -28
  32. package/src/hooks/ZoraV4CoinHook.sol +32 -15
  33. package/src/hooks/deployment/BuySupplyWithSwapRouterHook.sol +64 -4
  34. package/src/interfaces/IZoraFactory.sol +2 -1
  35. package/src/libs/CoinRewardsV4.sol +0 -1
  36. package/src/libs/HooksDeployment.sol +51 -7
  37. package/src/libs/UniV4SwapToCurrency.sol +3 -3
  38. package/src/libs/V4Liquidity.sol +2 -9
  39. package/src/version/ContractVersionBase.sol +1 -1
  40. package/test/CoinUniV4.t.sol +85 -60
  41. package/test/DeploymentHooks.t.sol +51 -10
  42. package/test/Upgrades.t.sol +92 -1
  43. package/test/utils/BaseTest.sol +56 -1
@@ -9,20 +9,31 @@ import {IWETH} from "../../interfaces/IWETH.sol";
9
9
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
10
10
  import {Coin} from "../../Coin.sol";
11
11
  import {ICoinV3} from "../../interfaces/ICoinV3.sol";
12
+ import {ICoinV4} from "../../interfaces/ICoinV4.sol";
13
+ import {CoinConfigurationVersions} from "../../libs/CoinConfigurationVersions.sol";
14
+ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
15
+ import {IPoolManager, SwapParams} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
16
+ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
17
+ import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
12
18
 
13
19
  /// @title BuySupplyWithSwapRouter
14
- /// @notice A hook that buys supply for a coin that is priced in an erc20 token with ETH, using a Uniswap SwapRouter.
15
- /// Supports both single-hop and multi-hop swaps using uniswap v3
20
+ /// @notice A hook that buys supply for a coin that is priced in an erc20 token a backing currency, using a Uniswap V3 SwapRouter.
21
+ /// Supports both single-hop and multi-hop swaps using uniswap v3. Supports buying the coin supply whether the coin is a v3 or v4 coin.
22
+ /// @author @oveddan
16
23
  contract BuySupplyWithSwapRouterHook is BaseCoinDeployHook {
17
24
  ISwapRouter immutable swapRouter;
25
+ IPoolManager immutable poolManager;
26
+ using BalanceDeltaLibrary for BalanceDelta;
18
27
 
19
28
  error Erc20NotReceived();
20
29
  error InvalidSwapRouterCall();
21
30
  error SwapReverted(bytes error);
22
31
  error CoinBalanceNot0(uint256 balance);
32
+ error CurrencyBalanceNot0(uint256 balance);
23
33
 
24
- constructor(IZoraFactory _factory, address _swapRouter) BaseCoinDeployHook(_factory) {
34
+ constructor(IZoraFactory _factory, address _swapRouter, address _poolManager) BaseCoinDeployHook(_factory) {
25
35
  swapRouter = ISwapRouter(_swapRouter);
36
+ poolManager = IPoolManager(_poolManager);
26
37
  }
27
38
 
28
39
  /// @notice Hook that buys supply for a coin that is priced in an erc20 token with ETH, using a Uniswap SwapRouter.
@@ -71,10 +82,59 @@ contract BuySupplyWithSwapRouterHook is BaseCoinDeployHook {
71
82
  function _handleBuy(address buyRecipient, ICoin coin, uint256 amountCurrency) internal returns (uint256 coinsPurchased) {
72
83
  IERC20(coin.currency()).approve(address(coin), amountCurrency);
73
84
 
74
- (, coinsPurchased) = ICoinV3(payable(address(coin))).buy(buyRecipient, amountCurrency, 0, 0, address(0));
85
+ if (CoinConfigurationVersions.isV4(factory.getVersionForDeployedCoin(address(coin)))) {
86
+ coinsPurchased = _executeV4Buy(buyRecipient, ICoinV4(payable(address(coin))), amountCurrency);
87
+ } else {
88
+ coinsPurchased = _executeV3Buy(buyRecipient, ICoinV3(payable(address(coin))), amountCurrency);
89
+ }
75
90
 
76
91
  // make sure that this contract has no balance of the coin remaining
77
92
  uint256 coinBalance = IERC20(address(coin)).balanceOf(address(this));
78
93
  require(coinBalance == 0, CoinBalanceNot0(coinBalance));
94
+ // make sure that this contract has no balance of the currency remaining
95
+ uint256 currencyBalance = IERC20(coin.currency()).balanceOf(address(this));
96
+ require(currencyBalance == 0, CurrencyBalanceNot0(currencyBalance));
97
+ }
98
+
99
+ function _executeV3Buy(address buyRecipient, ICoinV3 coin, uint256 amountCurrency) internal returns (uint256 coinsPurchased) {
100
+ (, coinsPurchased) = ICoinV3(payable(address(coin))).buy(buyRecipient, amountCurrency, 0, 0, address(0));
101
+ }
102
+
103
+ function _executeV4Buy(address buyRecipient, ICoinV4 coin, uint256 amountCurrency) internal returns (uint256 coinsPurchased) {
104
+ bytes memory data = abi.encode(buyRecipient, coin, amountCurrency);
105
+
106
+ bytes memory result = poolManager.unlock(data);
107
+
108
+ coinsPurchased = abi.decode(result, (uint256));
109
+ }
110
+
111
+ error OnlyPoolManager();
112
+
113
+ /// @notice Internal fn called when the PoolManager is unlocked. Used to swap the backing currency for the coin.
114
+ function unlockCallback(bytes calldata data) external returns (bytes memory) {
115
+ require(msg.sender == address(poolManager), OnlyPoolManager());
116
+
117
+ (address buyRecipient, ICoinV4 coin, uint256 amountCurrency) = abi.decode(data, (address, ICoinV4, uint256));
118
+
119
+ bool zeroForOne = coin.currency() == Currency.unwrap(coin.getPoolKey().currency0);
120
+
121
+ BalanceDelta delta = poolManager.swap(
122
+ coin.getPoolKey(),
123
+ SwapParams(zeroForOne, -(int128(uint128(amountCurrency))), zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),
124
+ ""
125
+ );
126
+
127
+ int128 amountCoin = zeroForOne ? delta.amount1() : delta.amount0();
128
+
129
+ // sync the currency balance before transferring to the pool manager
130
+ poolManager.sync(Currency.wrap(coin.currency()));
131
+ // transfer the currency to the pool manager for the swap
132
+ IERC20(coin.currency()).transfer(address(poolManager), uint256(uint128(amountCurrency)));
133
+ // collect the coin from the pool manager
134
+ poolManager.take(Currency.wrap(address(coin)), buyRecipient, uint256(uint128(amountCoin)));
135
+
136
+ poolManager.settle();
137
+
138
+ return abi.encode(uint256(uint128(amountCoin)));
79
139
  }
80
140
  }
@@ -3,8 +3,9 @@ pragma solidity ^0.8.23;
3
3
 
4
4
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
5
5
  import {PoolKeyStruct} from "./ICoin.sol";
6
+ import {IDeployedCoinVersionLookup} from "./IDeployedCoinVersionLookup.sol";
6
7
 
7
- interface IZoraFactory {
8
+ interface IZoraFactory is IDeployedCoinVersionLookup {
8
9
  /// @notice Emitted when a coin is created
9
10
  /// @param caller The msg.sender address
10
11
  /// @param payoutRecipient The address of the creator payout recipient
@@ -52,7 +52,6 @@ library CoinRewardsV4 {
52
52
  // Step 1: Collect accrued fees from all LP positions in both token0 and token1
53
53
  (fees0, fees1) = V4Liquidity.collectFees(poolManager, key, positions);
54
54
 
55
- // Step 2: Swap the collected fees through the specified path to convert them to the target payout currency
56
55
  // This handles multi-hop swaps if needed (e.g. coin -> backingCoin -> backingCoin's currency)
57
56
  (receivedCurrency, receivedAmount) = UniV4SwapToCurrency.swapToPath(
58
57
  poolManager,
@@ -17,6 +17,10 @@ library HookMinerWithCreationCodeArgs {
17
17
  // (arbitrarily set)
18
18
  uint256 constant MAX_LOOP = 160_444;
19
19
 
20
+ function deterministicHookAddress(address deployer, bytes32 salt, bytes memory creationCode) internal view returns (address) {
21
+ return Create2.computeAddress(salt, keccak256(creationCode), deployer);
22
+ }
23
+
20
24
  /// @notice Find a salt that produces a hook address with the desired `flags`
21
25
  /// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address
22
26
  /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy)
@@ -30,7 +34,7 @@ library HookMinerWithCreationCodeArgs {
30
34
 
31
35
  bytes32 creationCodeHash = keccak256(creationCodeWithArgs);
32
36
  for (uint256 salt; salt < MAX_LOOP; salt++) {
33
- hookAddress = Create2.computeAddress(bytes32(salt), creationCodeHash, deployer);
37
+ hookAddress = deterministicHookAddress(deployer, bytes32(salt), creationCodeWithArgs);
34
38
 
35
39
  // if the hook's bottom 14 bits match the desired flags AND the address does not have bytecode, we found a match
36
40
  if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) {
@@ -45,10 +49,17 @@ library HooksDeployment {
45
49
  error HookNotDeployed();
46
50
  error InvalidHookAddress(address expected, address actual);
47
51
 
48
- function deployZoraV4CoinHook(address deployer, bytes memory hookCreationCode) internal returns (IHooks hook) {
52
+ function mineForSaltAndDeployHook(address deployer, bytes memory hookCreationCode) internal returns (IHooks hook, bytes32 salt) {
49
53
  uint160 flags = uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG) ^ (0x4444 << 144);
50
54
 
51
- (address hookAddress, bytes32 salt) = HookMinerWithCreationCodeArgs.find(deployer, flags, hookCreationCode);
55
+ address hookAddress;
56
+ (hookAddress, salt) = HookMinerWithCreationCodeArgs.find(deployer, flags, hookCreationCode);
57
+
58
+ // check if the hook is already deployed
59
+ (bool isDeployed, address existingHookAddress) = hooksIsDeployed(deployer, hookCreationCode, salt);
60
+ if (isDeployed) {
61
+ return (IHooks(existingHookAddress), salt);
62
+ }
52
63
 
53
64
  hook = IHooks(Create2.deploy(0, salt, hookCreationCode));
54
65
 
@@ -57,6 +68,32 @@ library HooksDeployment {
57
68
  require(hookAddress == address(hook), InvalidHookAddress(hookAddress, address(hook)));
58
69
  }
59
70
 
71
+ /// @notice Checks if ZoraV4CoinHook is already deployed for given parameters
72
+ /// @param deployer The address that will deploy the hook
73
+ /// @param hookCreationCode The creation code of the hook
74
+ /// @param existingSalt The salt of the existing hook
75
+ /// @return isDeployed True if hook is already deployed
76
+ /// @return hookAddress The address where the hook would be/is deployed
77
+ function hooksIsDeployed(
78
+ address deployer,
79
+ bytes memory hookCreationCode,
80
+ bytes32 existingSalt
81
+ ) internal view returns (bool isDeployed, address hookAddress) {
82
+ hookAddress = HookMinerWithCreationCodeArgs.deterministicHookAddress(deployer, existingSalt, hookCreationCode);
83
+
84
+ // Check if code exists at the predicted address
85
+ isDeployed = hookAddress.code.length > 0;
86
+ }
87
+
88
+ function deployOrGetHook(address deployer, bytes memory hookCreationCode, bytes32 existingSalt) internal returns (IHooks hook, bytes32 salt) {
89
+ (bool isDeployed, address existingHookAddress) = hooksIsDeployed(deployer, hookCreationCode, existingSalt);
90
+ if (isDeployed) {
91
+ return (IHooks(existingHookAddress), existingSalt);
92
+ }
93
+
94
+ (hook, salt) = mineForSaltAndDeployHook(deployer, hookCreationCode);
95
+ }
96
+
60
97
  function zoraV4CoinHookCreationCode(
61
98
  address poolManager,
62
99
  address coinVersionLookup,
@@ -70,15 +107,22 @@ library HooksDeployment {
70
107
  address coinVersionLookup,
71
108
  address[] memory trustedMessageSenders
72
109
  ) internal returns (IHooks hook) {
73
- return deployZoraV4CoinHook(address(this), zoraV4CoinHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders));
110
+ bytes memory hookCreationCode = zoraV4CoinHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders);
111
+ (hook, ) = deployOrGetHook(address(this), hookCreationCode, bytes32(0));
74
112
  }
75
113
 
114
+ /// @notice Deploys or returns existing ZoraV4CoinHook using deterministic deployment. Ensures that if a hooks is already
115
+ /// deployed with an existing salt, it will be returned.
76
116
  function deployZoraV4CoinHookFromScript(
77
117
  address poolManager,
78
118
  address coinVersionLookup,
79
- address[] memory trustedMessageSenders
80
- ) internal returns (IHooks hook) {
119
+ address[] memory trustedMessageSenders,
120
+ bytes32 existingSalt
121
+ ) internal returns (IHooks hook, bytes32 salt) {
81
122
  address deployer = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
82
- return deployZoraV4CoinHook(deployer, zoraV4CoinHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders));
123
+
124
+ bytes memory hookCreationCode = zoraV4CoinHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders);
125
+
126
+ (hook, salt) = deployOrGetHook(deployer, hookCreationCode, existingSalt);
83
127
  }
84
128
  }
@@ -47,17 +47,17 @@ library UniV4SwapToCurrency {
47
47
  ) private returns (Currency outputCurrency, uint128 outputAmount) {
48
48
  (PoolKey memory poolKey, bool zeroForOne) = _getPoolAndSwapDirection(pathKey, coin);
49
49
 
50
- uint128 coinAmount = zeroForOne ? amount0 : amount1;
50
+ uint128 inputAmount = zeroForOne ? amount0 : amount1;
51
51
 
52
52
  outputCurrency = zeroForOne ? poolKey.currency1 : poolKey.currency0;
53
53
 
54
54
  uint128 initialAmountCurrency = zeroForOne ? amount1 : amount0;
55
55
 
56
56
  // if not swapping any coin for currency, output amount is amount of currency
57
- if (coinAmount == 0) {
57
+ if (inputAmount == 0) {
58
58
  outputAmount = initialAmountCurrency;
59
59
  } else {
60
- outputAmount = uint128(_swap(poolManager, poolKey, zeroForOne, -int128(coinAmount), bytes("")));
60
+ outputAmount = initialAmountCurrency + uint128(_swap(poolManager, poolKey, zeroForOne, -int128(inputAmount), bytes("")));
61
61
  }
62
62
  }
63
63
 
@@ -43,19 +43,12 @@ library V4Liquidity {
43
43
  IPoolManager(poolManager).unlock(data);
44
44
  }
45
45
 
46
- function handleMintPositionsCallback(IPoolManager poolManager, bytes memory data) internal returns (UnlockData memory) {
46
+ function handleMintPositionsCallback(IPoolManager poolManager, bytes memory data) internal {
47
47
  CallbackData memory callbackData = abi.decode(data, (CallbackData));
48
48
 
49
- uint256 amount0;
50
- uint256 amount1;
51
- int128 fees0;
52
- int128 fees1;
53
-
54
- (fees0, fees1) = _mintPositions(poolManager, callbackData.poolKey, callbackData.positions);
49
+ _mintPositions(poolManager, callbackData.poolKey, callbackData.positions);
55
50
 
56
51
  _settleUp(poolManager, callbackData.poolKey);
57
-
58
- return UnlockData({amount0: amount0, amount1: amount1, fees0: fees0, fees1: fees1});
59
52
  }
60
53
 
61
54
  function collectFees(IPoolManager poolManager, PoolKey memory poolKey, LpPosition[] storage positions) internal returns (int128 balance0, int128 balance1) {
@@ -9,6 +9,6 @@ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersion
9
9
  contract ContractVersionBase is IVersionedContract {
10
10
  /// @notice The version of the contract
11
11
  function contractVersion() external pure override returns (string memory) {
12
- return "1.0.0";
12
+ return "1.0.1";
13
13
  }
14
14
  }
@@ -36,8 +36,7 @@ contract CoinUniV4Test is BaseTest {
36
36
  CoinV4 internal coinV4;
37
37
 
38
38
  IPoolManager internal poolManager;
39
- IPermit2 internal permit2;
40
- IUniversalRouter internal router;
39
+
41
40
  IV4Quoter internal quoter;
42
41
  MockERC20 internal mockERC20A;
43
42
  MockERC20 internal mockERC20B;
@@ -46,8 +45,7 @@ contract CoinUniV4Test is BaseTest {
46
45
  super.setUpWithBlockNumber(30267794);
47
46
 
48
47
  poolManager = IPoolManager(V4_POOL_MANAGER);
49
- permit2 = IPermit2(V4_PERMIT2);
50
- router = IUniversalRouter(UNIVERSAL_ROUTER);
48
+
51
49
  quoter = IV4Quoter(V4_QUOTER);
52
50
  mockERC20A = new MockERC20("MockERC20A", "MCKA");
53
51
  mockERC20B = new MockERC20("MockERC20B", "MCKB");
@@ -67,6 +65,9 @@ contract CoinUniV4Test is BaseTest {
67
65
  return _deployV4Coin(currency, address(0), salt);
68
66
  }
69
67
 
68
+ string constant DEFAULT_NAME = "Testcoin";
69
+ string constant DEFAULT_SYMBOL = "TEST";
70
+
70
71
  function _deployV4Coin(address currency, address createReferral, bytes32 salt) internal returns (ICoinV4) {
71
72
  address[] memory owners = new address[](1);
72
73
  owners[0] = users.creator;
@@ -78,8 +79,8 @@ contract CoinUniV4Test is BaseTest {
78
79
  users.creator,
79
80
  owners,
80
81
  "https://test.com",
81
- "Testcoin",
82
- "TEST",
82
+ DEFAULT_NAME,
83
+ DEFAULT_SYMBOL,
83
84
  poolConfig,
84
85
  createReferral,
85
86
  address(0),
@@ -91,6 +92,10 @@ contract CoinUniV4Test is BaseTest {
91
92
  return coinV4;
92
93
  }
93
94
 
95
+ function _getCoinAddress(address currency, address createReferral, bytes32 salt) internal view returns (address) {
96
+ return factory.coinAddress(users.creator, DEFAULT_NAME, DEFAULT_SYMBOL, _defaultPoolConfig(currency), createReferral, salt);
97
+ }
98
+
94
99
  /// @dev Estimates the fees from a swap, by deploying a test hook that doesn't distribute the fees
95
100
  /// and then reverting the state after the swap
96
101
  function _estimateLpFees(bytes memory commands, bytes[] memory inputs) internal returns (FeeEstimatorHook.FeeEstimatorState memory feeState) {
@@ -106,50 +111,6 @@ contract CoinUniV4Test is BaseTest {
106
111
  vm.revertToState(snapshot);
107
112
  }
108
113
 
109
- function _swapSomeCurrencyForCoin(ICoinV4 _coin, address currency, uint128 amountIn, address trader) internal {
110
- uint128 minAmountOut = uint128(0);
111
-
112
- (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
113
- currency,
114
- amountIn,
115
- address(_coin),
116
- minAmountOut,
117
- _coin.getPoolKey(),
118
- bytes("")
119
- );
120
-
121
- vm.startPrank(trader);
122
- UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), currency, amountIn, uint48(block.timestamp + 1 days));
123
-
124
- // Execute the swap
125
- uint256 deadline = block.timestamp + 20;
126
- router.execute(commands, inputs, deadline);
127
-
128
- vm.stopPrank();
129
- }
130
-
131
- function _swapSomeCoinForCurrency(ICoinV4 _coin, address currency, uint128 amountIn, address trader) internal {
132
- uint128 minAmountOut = uint128(0);
133
-
134
- (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
135
- address(_coin),
136
- amountIn,
137
- currency,
138
- minAmountOut,
139
- _coin.getPoolKey(),
140
- bytes("")
141
- );
142
-
143
- vm.startPrank(trader);
144
- UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(_coin), amountIn, uint48(block.timestamp + 1 days));
145
-
146
- // Execute the swap
147
- uint256 deadline = block.timestamp + 20;
148
- router.execute(commands, inputs, deadline);
149
-
150
- vm.stopPrank();
151
- }
152
-
153
114
  /// and then reverting the state after the swap
154
115
  function _estimateSwap(
155
116
  bytes memory commands,
@@ -455,14 +416,57 @@ contract CoinUniV4Test is BaseTest {
455
416
  assertGt(amountOut, 0);
456
417
  }
457
418
 
458
- function test_canSwapCurrencyForCoin() public {
419
+ function _findCoinAddress(address currency, bool isCoinToken0) internal view returns (bytes32 salt, address coinAddress) {
420
+ uint256 i = 0;
421
+
422
+ while (true) {
423
+ salt = bytes32(keccak256(abi.encode(i)));
424
+ coinAddress = _getCoinAddress(currency, address(0), salt);
425
+ bool coinIsToken0 = coinAddress < currency;
426
+ if (coinIsToken0 == isCoinToken0) {
427
+ break;
428
+ }
429
+ i++;
430
+ }
431
+ }
432
+
433
+ function test_canSwapCurrencyForCoinCoinIsFirst(uint128 amountIn) public {
434
+ vm.assume(amountIn > 0.00001 ether);
435
+ vm.assume(amountIn < 10000000000000 ether);
459
436
  address currency = address(mockERC20A);
460
- _deployV4Coin(currency);
461
437
 
462
- uint128 amountIn = uint128(0.00001 ether);
438
+ (bytes32 salt, ) = _findCoinAddress(currency, true);
439
+
440
+ _deployV4Coin(currency, address(0), salt);
441
+
442
+ bool isCoinToken0 = CoinCommon.sortTokens(address(coinV4), currency);
443
+
444
+ assertTrue(isCoinToken0);
445
+
446
+ _testSwapCurrencyForCoin(currency, amountIn);
447
+ }
448
+
449
+ function test_canSwapCurrencyForCoinCoinIsSecond(uint128 amountIn) public {
450
+ vm.assume(amountIn > 0.00001 ether);
451
+ vm.assume(amountIn < 10000000000000 ether);
452
+
453
+ // make a currency with a small number, that will always be less than the coin
454
+ address currency = address(mockERC20A);
455
+
456
+ (bytes32 salt, ) = _findCoinAddress(currency, false);
457
+
458
+ _deployV4Coin(currency, address(0), salt);
463
459
 
460
+ assertTrue(coinV4.getPoolKey().currency0 == Currency.wrap(currency));
461
+
462
+ _testSwapCurrencyForCoin(currency, amountIn);
463
+ }
464
+
465
+ function _testSwapCurrencyForCoin(address currency, uint128 amountIn) private {
464
466
  uint128 minAmountOut = 0;
465
467
 
468
+ MockERC20(currency).mint(address(poolManager), 100000000000000000000000000000 ether);
469
+
466
470
  (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
467
471
  currency,
468
472
  amountIn,
@@ -474,10 +478,8 @@ contract CoinUniV4Test is BaseTest {
474
478
 
475
479
  address trader = makeAddr("trader");
476
480
 
477
- uint128 initialTraderBalance = uint128(1 ether);
478
-
479
481
  // mint some mockERC20 to the trader, so they can use it to buy the coin
480
- mockERC20A.mint(trader, initialTraderBalance);
482
+ mockERC20A.mint(trader, amountIn);
481
483
 
482
484
  // have trader approve to permit2
483
485
  vm.startPrank(trader);
@@ -487,16 +489,33 @@ contract CoinUniV4Test is BaseTest {
487
489
  uint256 deadline = block.timestamp + 20;
488
490
  router.execute(commands, inputs, deadline);
489
491
 
490
- assertEq(mockERC20A.balanceOf(trader), initialTraderBalance - amountIn);
492
+ assertEq(mockERC20A.balanceOf(trader), 0);
491
493
  assertGt(coinV4.balanceOf(trader), minAmountOut);
492
494
  }
493
495
 
494
- function test_canSwapCoinForCurrency() public {
496
+ function test_canSwapCoinForCurrencyCoinIsFirst(uint128 amountIn) public {
497
+ vm.assume(amountIn > 0.00001 ether);
498
+ vm.assume(amountIn < 10000000000000 ether);
499
+
495
500
  address currency = address(mockERC20A);
496
- _deployV4Coin(currency);
497
501
 
498
- uint128 currencyIn = uint128(0.00001 ether);
502
+ (bytes32 salt, ) = _findCoinAddress(currency, true);
503
+
504
+ _deployV4Coin(currency, address(0), salt);
505
+ }
506
+
507
+ function test_canSwapCoinForCurrencyCoinIsSecond(uint128 amountIn) public {
508
+ vm.assume(amountIn > 0.00001 ether);
509
+ vm.assume(amountIn < 10000000000000 ether);
510
+
511
+ address currency = address(mockERC20A);
499
512
 
513
+ (bytes32 salt, ) = _findCoinAddress(currency, false);
514
+
515
+ _deployV4Coin(currency, address(0), salt);
516
+ }
517
+
518
+ function _testSwapCoinForCurrency(address currency, uint128 currencyIn) private {
500
519
  address trader = makeAddr("trader");
501
520
 
502
521
  mockERC20A.mint(trader, currencyIn);
@@ -519,6 +538,12 @@ contract CoinUniV4Test is BaseTest {
519
538
  UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(coinV4), coinIn, uint48(block.timestamp + 1 days));
520
539
 
521
540
  router.execute(commands, inputs, block.timestamp + 20);
541
+
542
+ // do some more swaps back and forth
543
+ _swapSomeCurrencyForCoin(coinV4, currency, uint128(IERC20(address(currency)).balanceOf(trader)), trader);
544
+
545
+ // swap back to coin
546
+ _swapSomeCoinForCurrency(coinV4, currency, uint128(IERC20(address(coinV4)).balanceOf(trader)), trader);
522
547
  }
523
548
 
524
549
  function testSwappingEmitsSwapEventFromSenderNoRevert() public {
@@ -23,6 +23,7 @@ contract FakeHookNoInterface {
23
23
 
24
24
  contract HooksTest is BaseTest {
25
25
  address constant zora = 0x1111111111166b7FE7bd91427724B487980aFc69;
26
+ BuySupplyWithSwapRouterHook hook;
26
27
 
27
28
  function _generateDefaultPoolConfig(address currency) internal pure returns (bytes memory) {
28
29
  return
@@ -38,9 +39,11 @@ contract HooksTest is BaseTest {
38
39
 
39
40
  function setUp() public override {
40
41
  super.setUpWithBlockNumber(29585474);
42
+
43
+ hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter), address(V4_POOL_MANAGER));
41
44
  }
42
45
 
43
- function _deployWithHook(address hook, bytes memory hookData, address currency) internal returns (address, bytes memory) {
46
+ function _deployWithHook(address _hook, bytes memory hookData, address currency) internal returns (address, bytes memory) {
44
47
  bytes memory poolConfig = _generateDefaultPoolConfig(currency);
45
48
  return
46
49
  factory.deployWithHook(
@@ -51,7 +54,7 @@ contract HooksTest is BaseTest {
51
54
  "TEST",
52
55
  poolConfig,
53
56
  users.platformReferrer,
54
- hook,
57
+ _hook,
55
58
  hookData
56
59
  );
57
60
  }
@@ -74,8 +77,6 @@ contract HooksTest is BaseTest {
74
77
 
75
78
  vm.deal(users.creator, initialOrderSize);
76
79
 
77
- BuySupplyWithSwapRouterHook hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter));
78
-
79
80
  bytes memory hookData = _encodeExactInputSingle(
80
81
  users.creator,
81
82
  ISwapRouter.ExactInputSingleParams({
@@ -114,7 +115,7 @@ contract HooksTest is BaseTest {
114
115
  assertGt(IERC20(zora).balanceOf(address(pool)), 0, "Pool ZORA balance");
115
116
  }
116
117
 
117
- function test_buySupplyWithEthUsingV3Hook_withExactInputMultiHop(uint256 initialOrderSize) public {
118
+ function test_buySupplyWithEthUsingV4Hook_withExactInputMultiHop(uint256 initialOrderSize) public {
118
119
  vm.assume(initialOrderSize > CoinConstants.MIN_ORDER_SIZE);
119
120
  vm.assume(initialOrderSize < 1 ether);
120
121
 
@@ -122,7 +123,51 @@ contract HooksTest is BaseTest {
122
123
 
123
124
  // lets try weth to usdc to zora
124
125
 
125
- BuySupplyWithSwapRouterHook hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter));
126
+ uint24 poolFee = 3000;
127
+
128
+ bytes memory hookData = _encodeExactInput(
129
+ users.creator,
130
+ ISwapRouter.ExactInputParams({
131
+ path: abi.encodePacked(address(weth), poolFee, USDC_ADDRESS, poolFee, zora),
132
+ recipient: address(hook),
133
+ amountIn: initialOrderSize,
134
+ amountOutMinimum: 0
135
+ })
136
+ );
137
+
138
+ bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(zora);
139
+
140
+ vm.prank(users.creator);
141
+ (address coinAddress, bytes memory hookDataOut) = factory.deployWithHook{value: initialOrderSize}(
142
+ users.creator,
143
+ _getDefaultOwners(),
144
+ "https://test.com",
145
+ "Testcoin",
146
+ "TEST",
147
+ poolConfig,
148
+ users.platformReferrer,
149
+ address(hook),
150
+ hookData
151
+ );
152
+
153
+ coin = Coin(payable(coinAddress));
154
+
155
+ (uint256 amountCurrency, uint256 coinsPurchased) = abi.decode(hookDataOut, (uint256, uint256));
156
+
157
+ assertEq(coin.currency(), zora, "currency");
158
+ assertGt(amountCurrency, 0, "amountCurrency > 0");
159
+ assertGt(coinsPurchased, 0, "coinsPurchased > 0");
160
+ assertEq(coin.balanceOf(users.creator), CoinConstants.CREATOR_LAUNCH_REWARD + coinsPurchased, "balanceOf creator");
161
+ // assertGt(IERC20(zora).balanceOf(address(pool)), 0, "Pool ZORA balance");
162
+ }
163
+
164
+ function test_buySupplyWithEthUsingV3Hook_withExactInputMultiHop(uint256 initialOrderSize) public {
165
+ vm.assume(initialOrderSize > CoinConstants.MIN_ORDER_SIZE);
166
+ vm.assume(initialOrderSize < 1 ether);
167
+
168
+ vm.deal(users.creator, initialOrderSize);
169
+
170
+ // lets try weth to usdc to zora
126
171
 
127
172
  uint24 poolFee = 3000;
128
173
 
@@ -164,8 +209,6 @@ contract HooksTest is BaseTest {
164
209
  function test_buySupplyWithEthUsingV3Hook_revertsWhenBadCall() public {
165
210
  vm.deal(users.creator, 0.0001 ether);
166
211
 
167
- BuySupplyWithSwapRouterHook hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter));
168
-
169
212
  uint24 poolFee = 3000;
170
213
 
171
214
  // exact output single is not supported
@@ -201,8 +244,6 @@ contract HooksTest is BaseTest {
201
244
  uint256 initialOrderSize = 0.0001 ether;
202
245
  vm.deal(users.creator, initialOrderSize);
203
246
 
204
- BuySupplyWithSwapRouterHook hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter));
205
-
206
247
  bytes memory hookData = _encodeExactInputSingle(
207
248
  users.creator,
208
249
  ISwapRouter.ExactInputSingleParams({