@zoralabs/coins 0.9.0 → 1.0.0

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 (124) hide show
  1. package/.turbo/turbo-build.log +131 -114
  2. package/CHANGELOG.md +40 -0
  3. package/abis/BaseCoin.json +26 -118
  4. package/abis/BaseTest.json +47 -0
  5. package/abis/Coin.json +171 -63
  6. package/abis/CoinDopplerMultiCurve.json +38 -0
  7. package/abis/CoinRewardsV4.json +54 -0
  8. package/abis/CoinTest.json +53 -20
  9. package/abis/CoinUniV4Test.json +1053 -0
  10. package/abis/CoinV4.json +234 -211
  11. package/abis/DeployScript.json +47 -0
  12. package/abis/DeployedCoinVersionLookup.json +21 -0
  13. package/abis/DeployedCoinVersionLookupTest.json +716 -0
  14. package/abis/DifferentNamespaceVersionLookup.json +39 -0
  15. package/abis/DopplerUniswapV3Test.json +49 -93
  16. package/abis/ERC20.json +310 -0
  17. package/abis/FactoryTest.json +85 -7
  18. package/abis/FeeEstimatorHook.json +1528 -0
  19. package/abis/HooksDeployment.json +23 -0
  20. package/abis/HooksTest.json +47 -0
  21. package/abis/ICoin.json +40 -71
  22. package/abis/ICoinV3.json +879 -0
  23. package/abis/ICoinV4.json +915 -0
  24. package/abis/IDeployedCoinVersionLookup.json +21 -0
  25. package/abis/IERC721.json +36 -36
  26. package/abis/IHasPoolKey.json +42 -0
  27. package/abis/IHasRewardsRecipients.json +54 -0
  28. package/abis/IHasSwapPath.json +60 -0
  29. package/abis/IMsgSender.json +15 -0
  30. package/abis/IPoolConfigEncoding.json +46 -0
  31. package/abis/ISwapPathRouter.json +92 -0
  32. package/abis/IUniversalRouter.json +61 -0
  33. package/abis/IUnlockCallback.json +21 -0
  34. package/abis/IV4Quoter.json +310 -0
  35. package/abis/IZoraFactory.json +191 -11
  36. package/abis/IZoraV4CoinHook.json +348 -4
  37. package/abis/MockERC20.json +21 -0
  38. package/abis/MultiOwnableTest.json +47 -0
  39. package/abis/{CoinConfigurationVersions.json → Position.json} +1 -1
  40. package/abis/PrintUpgradeCommand.json +9 -0
  41. package/abis/ProxyShim.json +24 -0
  42. package/abis/StateLibrary.json +80 -0
  43. package/abis/TestDeployedCoinVersionLookupImplementation.json +39 -0
  44. package/abis/TestV4Swap.json +9 -0
  45. package/abis/UpgradeCoinImpl.json +47 -0
  46. package/abis/UpgradesTest.json +67 -0
  47. package/abis/Vm.json +1482 -111
  48. package/abis/VmSafe.json +856 -32
  49. package/abis/ZoraFactoryImpl.json +339 -1
  50. package/abis/ZoraV4CoinHook.json +455 -5
  51. package/addresses/8453.json +8 -4
  52. package/addresses/84532.json +8 -4
  53. package/dist/index.cjs +1920 -169
  54. package/dist/index.cjs.map +1 -1
  55. package/dist/index.js +1916 -169
  56. package/dist/index.js.map +1 -1
  57. package/dist/wagmiGenerated.d.ts +2599 -183
  58. package/dist/wagmiGenerated.d.ts.map +1 -1
  59. package/package/wagmiGenerated.ts +1928 -165
  60. package/package.json +8 -3
  61. package/remappings.txt +6 -1
  62. package/script/CoinsDeployerBase.sol +74 -11
  63. package/script/DeployDevFactory.s.sol +21 -0
  64. package/script/PrintUpgradeCommand.s.sol +13 -0
  65. package/script/Simulate.s.sol +1 -10
  66. package/script/TestBackingCoinSwap.s.sol +146 -0
  67. package/script/TestV4Swap.s.sol +136 -0
  68. package/script/UpgradeFactoryImpl.s.sol +1 -1
  69. package/src/BaseCoin.sol +176 -0
  70. package/src/Coin.sol +87 -202
  71. package/src/CoinV4.sol +121 -0
  72. package/src/ZoraFactoryImpl.sol +208 -36
  73. package/src/hooks/ZoraV4CoinHook.sol +195 -0
  74. package/src/hooks/{BaseCoinDeployHook.sol → deployment/BaseCoinDeployHook.sol} +3 -3
  75. package/src/hooks/{BuySupplyWithSwapRouterHook.sol → deployment/BuySupplyWithSwapRouterHook.sol} +7 -5
  76. package/src/interfaces/ICoin.sol +31 -39
  77. package/src/interfaces/ICoinV3.sol +71 -0
  78. package/src/interfaces/ICoinV4.sol +69 -0
  79. package/src/interfaces/IDeployedCoinVersionLookup.sol +11 -0
  80. package/src/interfaces/IMsgSender.sol +9 -0
  81. package/src/interfaces/IPoolConfigEncoding.sol +14 -0
  82. package/src/interfaces/ISwapPathRouter.sol +14 -0
  83. package/src/interfaces/IZoraFactory.sol +65 -27
  84. package/src/interfaces/IZoraV4CoinHook.sol +116 -0
  85. package/src/libs/CoinCommon.sol +15 -0
  86. package/src/libs/CoinConfigurationVersions.sol +116 -1
  87. package/src/libs/CoinConstants.sol +5 -0
  88. package/src/libs/CoinDopplerMultiCurve.sol +134 -0
  89. package/src/libs/CoinDopplerUniV3.sol +19 -171
  90. package/src/libs/CoinRewards.sol +195 -0
  91. package/src/libs/CoinRewardsV4.sol +180 -0
  92. package/src/libs/CoinSetup.sol +57 -0
  93. package/src/libs/CoinSetupV3.sol +6 -67
  94. package/src/libs/DopplerMath.sol +156 -0
  95. package/src/libs/HooksDeployment.sol +84 -0
  96. package/src/libs/MarketConstants.sol +4 -0
  97. package/src/libs/PoolStateReader.sol +22 -0
  98. package/src/libs/UniV3BuySell.sol +74 -292
  99. package/src/libs/UniV4SwapHelper.sol +65 -0
  100. package/src/libs/UniV4SwapToCurrency.sol +109 -0
  101. package/src/libs/V4Liquidity.sol +129 -0
  102. package/src/types/PoolConfiguration.sol +15 -0
  103. package/src/utils/DeployedCoinVersionLookup.sol +52 -0
  104. package/src/version/ContractVersionBase.sol +1 -1
  105. package/test/Coin.t.sol +78 -88
  106. package/test/CoinDopplerUniV3.t.sol +32 -171
  107. package/test/CoinUniV4.t.sol +752 -0
  108. package/test/{Hooks.t.sol → DeploymentHooks.t.sol} +2 -6
  109. package/test/Factory.t.sol +80 -47
  110. package/test/MultiOwnable.t.sol +6 -3
  111. package/test/Upgrades.t.sol +6 -5
  112. package/test/mocks/MockERC20.sol +12 -0
  113. package/test/utils/BaseTest.sol +106 -56
  114. package/test/utils/DeployedCoinVersionLookup.t.sol +127 -0
  115. package/test/utils/FeeEstimatorHook.sol +84 -0
  116. package/test/utils/ProxyShim.sol +17 -0
  117. package/wagmi.config.ts +4 -0
  118. package/.env +0 -1
  119. package/.turbo/turbo-update-contract-version.log +0 -22
  120. package/abis/CoinSetupV3.json +0 -7
  121. package/abis/HookDeployer.json +0 -68
  122. package/abis/IHookDeployer.json +0 -42
  123. package/src/libs/CoinLegacy.sol +0 -48
  124. package/src/libs/CoinLegacyMarket.sol +0 -182
@@ -0,0 +1,752 @@
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 {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
7
+ import {IV4Quoter} from "@uniswap/v4-periphery/src/interfaces/IV4Quoter.sol";
8
+ import {IUniversalRouter} from "@uniswap/universal-router/contracts/interfaces/IUniversalRouter.sol";
9
+ import {Commands} from "@uniswap/universal-router/contracts/libraries/Commands.sol";
10
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
11
+ import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
12
+ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
13
+ import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
14
+ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
15
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
16
+ import {LpPosition} from "../src/types/LpPosition.sol";
17
+ import {CoinCommon} from "../src/libs/CoinCommon.sol";
18
+ import {IZoraV4CoinHook} from "../src/interfaces/IZoraV4CoinHook.sol";
19
+ import {IMsgSender} from "../src/interfaces/IMsgSender.sol";
20
+ import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
21
+ import {toBalanceDelta, BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
22
+ import {UniV4SwapHelper} from "../src/libs/UniV4SwapHelper.sol";
23
+ import {MockERC20} from "./mocks/MockERC20.sol";
24
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
25
+ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
26
+ import {FeeEstimatorHook} from "./utils/FeeEstimatorHook.sol";
27
+ import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
28
+ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
29
+ import {PoolStateReader} from "../src/libs/PoolStateReader.sol";
30
+ import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol";
31
+ import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
32
+ import {ICoinV4, IHasSwapPath, PathKey} from "../src/interfaces/ICoinV4.sol";
33
+ import {IDeployedCoinVersionLookup} from "../src/interfaces/IDeployedCoinVersionLookup.sol";
34
+
35
+ contract CoinUniV4Test is BaseTest {
36
+ CoinV4 internal coinV4;
37
+
38
+ IPoolManager internal poolManager;
39
+ IPermit2 internal permit2;
40
+ IUniversalRouter internal router;
41
+ IV4Quoter internal quoter;
42
+ MockERC20 internal mockERC20A;
43
+ MockERC20 internal mockERC20B;
44
+
45
+ function setUp() public override {
46
+ super.setUpWithBlockNumber(30267794);
47
+
48
+ poolManager = IPoolManager(V4_POOL_MANAGER);
49
+ permit2 = IPermit2(V4_PERMIT2);
50
+ router = IUniversalRouter(UNIVERSAL_ROUTER);
51
+ quoter = IV4Quoter(V4_QUOTER);
52
+ mockERC20A = new MockERC20("MockERC20A", "MCKA");
53
+ mockERC20B = new MockERC20("MockERC20B", "MCKB");
54
+
55
+ // make sure the pool manager has some of the backing liquidity
56
+ // so we can take the fees in the first swap
57
+ mockERC20A.mint(address(poolManager), 1000000 ether);
58
+ mockERC20B.mint(address(poolManager), 1000000 ether);
59
+ }
60
+
61
+ function _defaultPoolConfig(address currency) internal pure returns (bytes memory) {
62
+ return CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(currency);
63
+ }
64
+
65
+ function _deployV4Coin(address currency) internal returns (ICoinV4) {
66
+ bytes32 salt = keccak256(abi.encode(bytes("randomSalt")));
67
+ return _deployV4Coin(currency, address(0), salt);
68
+ }
69
+
70
+ function _deployV4Coin(address currency, address createReferral, bytes32 salt) internal returns (ICoinV4) {
71
+ address[] memory owners = new address[](1);
72
+ owners[0] = users.creator;
73
+
74
+ bytes memory poolConfig = _defaultPoolConfig(currency);
75
+
76
+ vm.prank(users.creator);
77
+ (address coinAddress, ) = factory.deploy(
78
+ users.creator,
79
+ owners,
80
+ "https://test.com",
81
+ "Testcoin",
82
+ "TEST",
83
+ poolConfig,
84
+ createReferral,
85
+ address(0),
86
+ bytes(""),
87
+ salt
88
+ );
89
+
90
+ coinV4 = CoinV4(payable(coinAddress));
91
+ return coinV4;
92
+ }
93
+
94
+ /// @dev Estimates the fees from a swap, by deploying a test hook that doesn't distribute the fees
95
+ /// and then reverting the state after the swap
96
+ function _estimateLpFees(bytes memory commands, bytes[] memory inputs) internal returns (FeeEstimatorHook.FeeEstimatorState memory feeState) {
97
+ uint256 snapshot = vm.snapshot();
98
+ deployCodeTo("FeeEstimatorHook.sol", abi.encode(address(poolManager), address(factory)), address(coinV4.hooks()));
99
+
100
+ // Execute the swap
101
+ uint256 deadline = block.timestamp + 20;
102
+ router.execute(commands, inputs, deadline);
103
+
104
+ feeState = FeeEstimatorHook(address(coinV4.hooks())).getFeeState();
105
+
106
+ vm.revertToState(snapshot);
107
+ }
108
+
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
+ /// and then reverting the state after the swap
154
+ function _estimateSwap(
155
+ bytes memory commands,
156
+ bytes[] memory inputs
157
+ ) internal returns (BalanceDelta delta, SwapParams memory swapParams, uint160 sqrtPriceX96) {
158
+ uint256 snapshot = vm.snapshot();
159
+ deployCodeTo("FeeEstimatorHook.sol", abi.encode(address(poolManager), address(factory)), address(coinV4.hooks()));
160
+
161
+ // Execute the swap
162
+ uint256 deadline = block.timestamp + 20;
163
+ router.execute(commands, inputs, deadline);
164
+
165
+ delta = FeeEstimatorHook(address(coinV4.hooks())).getFeeState().lastDelta;
166
+ swapParams = FeeEstimatorHook(address(coinV4.hooks())).getFeeState().lastSwapParams;
167
+
168
+ sqrtPriceX96 = PoolStateReader.getSqrtPriceX96(coinV4.getPoolKey(), poolManager);
169
+
170
+ vm.revertToState(snapshot);
171
+ }
172
+
173
+ function test_setupZeroAddressForPoolManager() public {
174
+ vm.expectRevert(ICoin.AddressZero.selector);
175
+ new CoinV4({
176
+ protocolRewardRecipient_: address(0x1234),
177
+ protocolRewards_: address(0x1234),
178
+ poolManager_: IPoolManager(address(0)),
179
+ airlock_: address(0),
180
+ hooks_: IHooks(address(0))
181
+ });
182
+ }
183
+
184
+ function test_setupZeroAddressForHooks() public {
185
+ vm.expectRevert(ICoin.AddressZero.selector);
186
+ new CoinV4({
187
+ protocolRewardRecipient_: address(0x1234),
188
+ protocolRewards_: address(0x1234),
189
+ poolManager_: IPoolManager(address(0x1234)),
190
+ airlock_: address(0x1234),
191
+ hooks_: IHooks(address(0))
192
+ });
193
+ }
194
+
195
+ function test_estimateLpFees() public {
196
+ address currency = address(mockERC20A);
197
+ _deployV4Coin(currency);
198
+
199
+ uint128 amountIn = uint128(0.00001 ether);
200
+ uint128 minAmountOut = uint128(0);
201
+
202
+ (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
203
+ currency,
204
+ amountIn,
205
+ address(coinV4),
206
+ minAmountOut,
207
+ coinV4.getPoolKey(),
208
+ bytes("")
209
+ );
210
+
211
+ address trader = makeAddr("trader");
212
+
213
+ // mint some mockERC20 to the trader, so they can use it to buy the coin
214
+ mockERC20A.mint(trader, 1 ether);
215
+
216
+ // have trader approve to permit2
217
+ vm.startPrank(trader);
218
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), currency, type(uint128).max, uint48(block.timestamp + 1 days));
219
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(coinV4), type(uint128).max, uint48(block.timestamp + 1 days));
220
+
221
+ // do a fake swap, so we can estimate LP fees
222
+ FeeEstimatorHook.FeeEstimatorState memory feeState = _estimateLpFees(commands, inputs);
223
+
224
+ bool isCoinToken0 = Currency.unwrap(coinV4.getPoolKey().currency0) == address(coinV4);
225
+ uint128 feeCoin = isCoinToken0 ? feeState.fees0 : feeState.fees1;
226
+ uint128 feeCurrency = isCoinToken0 ? feeState.fees1 : feeState.fees0;
227
+
228
+ assertEq(feeCoin, 0, "fee coin should be 0");
229
+ assertGt(feeCurrency, 0, "fee currency should be greater than to 0");
230
+ assertGt(feeState.afterSwapCurrencyAmount, 0, "after swap fee currency should be greater than 0");
231
+
232
+ // execute the swap
233
+ router.execute(commands, inputs, block.timestamp + 20);
234
+
235
+ // now estimate fees in swap back to currency
236
+ (commands, inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
237
+ address(coinV4),
238
+ feeCurrency,
239
+ currency,
240
+ minAmountOut,
241
+ coinV4.getPoolKey(),
242
+ bytes("")
243
+ );
244
+
245
+ FeeEstimatorHook.FeeEstimatorState memory newFeeState = _estimateLpFees(commands, inputs);
246
+
247
+ uint128 newFeeCoin = isCoinToken0 ? newFeeState.fees0 : newFeeState.fees1;
248
+ uint128 newFeeCurrency = isCoinToken0 ? newFeeState.fees1 : newFeeState.fees0;
249
+
250
+ assertGt(newFeeCoin, 0, "fee coin on second swap should be greater than 0");
251
+ assertEq(newFeeCurrency, 0, "fee currency on second swap should be greater than 0");
252
+ assertGt(newFeeState.afterSwapCurrencyAmount, 0, "after swap fee currency on second swap should be greater than 0");
253
+ }
254
+
255
+ uint256 public constant CREATOR_REWARD_BPS = 5000;
256
+ uint256 public constant CREATE_REFERRAL_REWARD_BPS = 1500;
257
+ uint256 public constant TRADE_REFERRAL_REWARD_BPS = 1500;
258
+ uint256 public constant DOPPLER_REWARD_BPS = 500;
259
+
260
+ function computeExpectedRewards(
261
+ uint256 fee,
262
+ bool hasCreateReferral,
263
+ bool hasTradeReferral
264
+ )
265
+ internal
266
+ pure
267
+ returns (
268
+ uint256 backingRewardsCurrency,
269
+ uint256 dopplerRewardsCurrency,
270
+ uint256 createReferralRewardsCurrency,
271
+ uint256 tradeReferralRewardsCurrency,
272
+ uint256 protocolRewardsCurrency
273
+ )
274
+ {
275
+ backingRewardsCurrency = CoinRewardsV4.calculateReward(fee, CREATOR_REWARD_BPS);
276
+ dopplerRewardsCurrency = CoinRewardsV4.calculateReward(fee, DOPPLER_REWARD_BPS);
277
+ createReferralRewardsCurrency = hasCreateReferral ? CoinRewardsV4.calculateReward(fee, CREATE_REFERRAL_REWARD_BPS) : 0;
278
+ tradeReferralRewardsCurrency = hasTradeReferral ? CoinRewardsV4.calculateReward(fee, TRADE_REFERRAL_REWARD_BPS) : 0;
279
+ protocolRewardsCurrency = fee - backingRewardsCurrency - dopplerRewardsCurrency - createReferralRewardsCurrency - tradeReferralRewardsCurrency;
280
+ }
281
+
282
+ function test_distributesMarketRewards(uint64 amountIn, bool hasCreateReferral, bool hasTradeReferral) public {
283
+ vm.assume(amountIn > 0.00001 ether);
284
+ address currency = address(mockERC20A);
285
+ address createReferral = hasCreateReferral ? makeAddr("createReferral") : address(0);
286
+ address tradeReferral = hasTradeReferral ? makeAddr("tradeReferral") : address(0);
287
+ bytes32 salt = keccak256(abi.encodePacked(amountIn, hasCreateReferral, hasTradeReferral));
288
+ _deployV4Coin(currency, createReferral, salt);
289
+
290
+ uint256 balanceBeforePayoutRecipient = coinV4.balanceOf(coinV4.payoutRecipient());
291
+
292
+ uint128 minAmountOut = uint128(0);
293
+
294
+ bytes memory hookData = hasTradeReferral ? abi.encode(tradeReferral) : bytes("");
295
+
296
+ (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
297
+ currency,
298
+ amountIn,
299
+ address(coinV4),
300
+ minAmountOut,
301
+ coinV4.getPoolKey(),
302
+ hookData
303
+ );
304
+
305
+ address trader = makeAddr("trader");
306
+
307
+ // mint some mockERC20 to the trader, so they can use it to buy the coin
308
+ mockERC20A.mint(trader, amountIn);
309
+
310
+ // have trader approve to permit2
311
+ vm.startPrank(trader);
312
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), currency, amountIn, uint48(block.timestamp + 1 days));
313
+
314
+ // do a fake swap, so we can estimate LP fees
315
+ FeeEstimatorHook.FeeEstimatorState memory feeState = _estimateLpFees(commands, inputs);
316
+
317
+ (
318
+ uint256 backingRewardsCurrency,
319
+ uint256 dopplerRewardsCurrency,
320
+ uint256 createReferralRewardsCurrency,
321
+ uint256 tradeReferralRewardsCurrency,
322
+ uint256 protocolRewardsCurrency
323
+ ) = computeExpectedRewards(feeState.afterSwapCurrencyAmount, hasCreateReferral, hasTradeReferral);
324
+
325
+ // Execute the swap
326
+ router.execute(commands, inputs, block.timestamp + 20);
327
+
328
+ // now do a swap back to the currency
329
+ (commands, inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(address(coinV4), amountIn, currency, minAmountOut, coinV4.getPoolKey(), hookData);
330
+
331
+ // approve the coinV4 to spend the coin
332
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(coinV4), amountIn, uint48(block.timestamp + 1 days));
333
+
334
+ // estimate the new LP fees
335
+ FeeEstimatorHook.FeeEstimatorState memory newFeeState = _estimateLpFees(commands, inputs);
336
+
337
+ (
338
+ uint256 backingRewardsCurrency2,
339
+ uint256 dopplerRewardsCurrency2,
340
+ uint256 createReferralRewardsCurrency2,
341
+ uint256 tradeReferralRewardsCurrency2,
342
+ uint256 protocolRewardsCurrency2
343
+ ) = computeExpectedRewards(newFeeState.afterSwapCurrencyAmount, hasCreateReferral, hasTradeReferral);
344
+
345
+ // now do a swap, rewards balance changes of both the coin and the currency should reflect the new fees
346
+ router.execute(commands, inputs, block.timestamp + 20);
347
+
348
+ assertEq(coinV4.balanceOf(coinV4.payoutRecipient()) - balanceBeforePayoutRecipient, 0, "backing reward coin");
349
+ assertEq(coinV4.balanceOf(coinV4.dopplerFeeRecipient()), 0, "doppler reward coin");
350
+ if (hasCreateReferral) {
351
+ assertEq(coinV4.balanceOf(createReferral), 0, "create referral reward coin");
352
+ }
353
+ if (hasTradeReferral) {
354
+ assertEq(coinV4.balanceOf(tradeReferral), 0, "trade referral reward coin");
355
+ }
356
+ assertEq(coinV4.balanceOf(coinV4.protocolRewardRecipient()), 0, "protocol reward coin");
357
+
358
+ assertEq(mockERC20A.balanceOf(coinV4.payoutRecipient()), backingRewardsCurrency + backingRewardsCurrency2, "backing reward currency");
359
+ assertEq(mockERC20A.balanceOf(coinV4.dopplerFeeRecipient()), dopplerRewardsCurrency + dopplerRewardsCurrency2, "doppler reward currency");
360
+ if (hasCreateReferral) {
361
+ assertEq(mockERC20A.balanceOf(createReferral), createReferralRewardsCurrency + createReferralRewardsCurrency2, "create referral reward currency");
362
+ }
363
+ if (hasTradeReferral) {
364
+ assertEq(mockERC20A.balanceOf(tradeReferral), tradeReferralRewardsCurrency + tradeReferralRewardsCurrency2, "trade referral reward currency");
365
+ }
366
+ assertEq(mockERC20A.balanceOf(coinV4.protocolRewardRecipient()), protocolRewardsCurrency + protocolRewardsCurrency2, "protocol reward currency");
367
+ }
368
+
369
+ function test_swap_emitsCoinMarketRewardsV4(uint64 amountIn) public {
370
+ vm.assume(amountIn > 0.00001 ether);
371
+ address currency = address(mockERC20A);
372
+ address createReferral = makeAddr("createReferral");
373
+ address tradeReferral = makeAddr("tradeReferral");
374
+ bytes32 salt = keccak256(abi.encode(bytes("randomSalt")));
375
+ _deployV4Coin(currency, createReferral, salt);
376
+
377
+ uint128 minAmountOut = uint128(0);
378
+
379
+ bytes memory hookData = abi.encode(tradeReferral);
380
+
381
+ (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
382
+ currency,
383
+ amountIn,
384
+ address(coinV4),
385
+ minAmountOut,
386
+ coinV4.getPoolKey(),
387
+ hookData
388
+ );
389
+
390
+ address trader = makeAddr("trader");
391
+
392
+ // mint some mockERC20 to the trader, so they can use it to buy the coin
393
+ mockERC20A.mint(trader, amountIn);
394
+
395
+ // have trader approve to permit2
396
+ vm.startPrank(trader);
397
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), currency, amountIn, uint48(block.timestamp + 1 days));
398
+
399
+ // do a fake swap, so we can estimate LP fees
400
+ FeeEstimatorHook.FeeEstimatorState memory feeState = _estimateLpFees(commands, inputs);
401
+
402
+ (
403
+ uint256 backingRewardsCurrency,
404
+ uint256 dopplerRewardsCurrency,
405
+ uint256 createReferralRewardsCurrency,
406
+ uint256 tradeReferralRewardsCurrency,
407
+ uint256 protocolRewardsCurrency
408
+ ) = computeExpectedRewards(feeState.afterSwapCurrencyAmount, true, true);
409
+
410
+ vm.expectEmit(true, true, true, true);
411
+ emit IZoraV4CoinHook.CoinMarketRewardsV4(
412
+ address(coinV4),
413
+ address(mockERC20A),
414
+ coinV4.payoutRecipient(),
415
+ createReferral,
416
+ tradeReferral,
417
+ coinV4.protocolRewardRecipient(),
418
+ coinV4.dopplerFeeRecipient(),
419
+ IZoraV4CoinHook.MarketRewardsV4({
420
+ creatorPayoutAmountCurrency: backingRewardsCurrency,
421
+ creatorPayoutAmountCoin: 0,
422
+ platformReferrerAmountCurrency: createReferralRewardsCurrency,
423
+ platformReferrerAmountCoin: 0,
424
+ tradeReferrerAmountCurrency: tradeReferralRewardsCurrency,
425
+ tradeReferrerAmountCoin: 0,
426
+ protocolAmountCurrency: protocolRewardsCurrency,
427
+ protocolAmountCoin: 0,
428
+ dopplerAmountCurrency: dopplerRewardsCurrency,
429
+ dopplerAmountCoin: 0
430
+ })
431
+ );
432
+
433
+ // Execute the swap
434
+ router.execute(commands, inputs, block.timestamp + 20);
435
+ }
436
+
437
+ function test_canGetQuoteForSwappingCurrencyForCoin() public {
438
+ address currency = address(mockERC20A);
439
+ _deployV4Coin(currency);
440
+
441
+ bool isCoinToken0 = CoinCommon.sortTokens(address(coinV4), currency);
442
+
443
+ // we want to swap currency for coin
444
+ bool zeroForOne = !isCoinToken0;
445
+
446
+ IV4Quoter.QuoteExactSingleParams memory quoteParams = IV4Quoter.QuoteExactSingleParams({
447
+ poolKey: coinV4.getPoolKey(),
448
+ zeroForOne: zeroForOne,
449
+ exactAmount: 1 ether,
450
+ hookData: bytes("")
451
+ });
452
+
453
+ (uint256 amountOut, ) = quoter.quoteExactInputSingle(quoteParams);
454
+
455
+ assertGt(amountOut, 0);
456
+ }
457
+
458
+ function test_canSwapCurrencyForCoin() public {
459
+ address currency = address(mockERC20A);
460
+ _deployV4Coin(currency);
461
+
462
+ uint128 amountIn = uint128(0.00001 ether);
463
+
464
+ uint128 minAmountOut = 0;
465
+
466
+ (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
467
+ currency,
468
+ amountIn,
469
+ address(coinV4),
470
+ minAmountOut,
471
+ coinV4.getPoolKey(),
472
+ bytes("")
473
+ );
474
+
475
+ address trader = makeAddr("trader");
476
+
477
+ uint128 initialTraderBalance = uint128(1 ether);
478
+
479
+ // mint some mockERC20 to the trader, so they can use it to buy the coin
480
+ mockERC20A.mint(trader, initialTraderBalance);
481
+
482
+ // have trader approve to permit2
483
+ vm.startPrank(trader);
484
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), currency, amountIn, uint48(block.timestamp + 1 days));
485
+
486
+ // Execute the swap
487
+ uint256 deadline = block.timestamp + 20;
488
+ router.execute(commands, inputs, deadline);
489
+
490
+ assertEq(mockERC20A.balanceOf(trader), initialTraderBalance - amountIn);
491
+ assertGt(coinV4.balanceOf(trader), minAmountOut);
492
+ }
493
+
494
+ function test_canSwapCoinForCurrency() public {
495
+ address currency = address(mockERC20A);
496
+ _deployV4Coin(currency);
497
+
498
+ uint128 currencyIn = uint128(0.00001 ether);
499
+
500
+ address trader = makeAddr("trader");
501
+
502
+ mockERC20A.mint(trader, currencyIn);
503
+
504
+ // swap some currency for coin so that the pool has some balance to work with
505
+ _swapSomeCurrencyForCoin(coinV4, currency, currencyIn, trader);
506
+
507
+ uint128 coinIn = uint128(coinV4.balanceOf(trader));
508
+ // now swap coin for currency
509
+ (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
510
+ address(coinV4),
511
+ coinIn,
512
+ currency,
513
+ 0,
514
+ coinV4.getPoolKey(),
515
+ bytes("")
516
+ );
517
+
518
+ vm.startPrank(trader);
519
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(coinV4), coinIn, uint48(block.timestamp + 1 days));
520
+
521
+ router.execute(commands, inputs, block.timestamp + 20);
522
+ }
523
+
524
+ function testSwappingEmitsSwapEventFromSenderNoRevert() public {
525
+ _callSwappingEmitsSwapEvent(false, false);
526
+ }
527
+
528
+ function testSwappingEmitsSwapEventFromSenderReverts() public {
529
+ _callSwappingEmitsSwapEvent(false, true);
530
+ }
531
+
532
+ function testSwappingEmitsSwapEventFromTrustedMessageSenderNoRevert() public {
533
+ _callSwappingEmitsSwapEvent(true, false);
534
+ }
535
+
536
+ function testSwappingEmitsSwapEventFromTrustedMessageSenderReverts() public {
537
+ _callSwappingEmitsSwapEvent(true, true);
538
+ }
539
+
540
+ function test_afterInitializeRevertsWhenSenderIsNotACoin() public {
541
+ // First deploy a coin so we have a valid hook to test against
542
+ address currency = address(mockERC20A);
543
+ _deployV4Coin(currency);
544
+
545
+ // Deploy a mock contract that is not a coin
546
+ MockERC20 notACoin = new MockERC20("NotACoin", "NAC");
547
+
548
+ bool isCoinToken0 = CoinCommon.sortTokens(address(coinV4), address(notACoin));
549
+
550
+ // Create a valid pool key
551
+ PoolKey memory key = PoolKey({
552
+ currency0: isCoinToken0 ? Currency.wrap(address(coinV4)) : Currency.wrap(address(notACoin)),
553
+ currency1: isCoinToken0 ? Currency.wrap(address(notACoin)) : Currency.wrap(address(coinV4)),
554
+ fee: 3000,
555
+ tickSpacing: 60,
556
+ hooks: IHooks(address(coinV4.hooks()))
557
+ });
558
+
559
+ // We need to prank the call to come from the non-coin contract
560
+ vm.startPrank(address(notACoin));
561
+
562
+ // The hook should revert with NotACoin error when initializing
563
+ vm.expectRevert(
564
+ abi.encodeWithSelector(
565
+ CustomRevert.WrappedError.selector,
566
+ address(coinV4.hooks()),
567
+ IHooks.afterInitialize.selector,
568
+ abi.encodeWithSelector(IZoraV4CoinHook.NotACoin.selector, address(notACoin)),
569
+ abi.encodeWithSelector(Hooks.HookCallFailed.selector)
570
+ )
571
+ );
572
+
573
+ // Call the pool manager to initialize, the hook should revert because the calling coin is not a coin
574
+ poolManager.initialize(key, uint160(1049428825694136384760392514097686388));
575
+
576
+ vm.stopPrank();
577
+ }
578
+
579
+ function _callSwappingEmitsSwapEvent(bool swapIsFromTrustedMessageSender, bool trustedSenderReverts) internal {
580
+ uint64 amountIn = uint64(0.1 ether);
581
+ address currency = address(mockERC20A);
582
+ _deployV4Coin(currency);
583
+
584
+ uint128 minAmountOut = uint128(0);
585
+
586
+ PoolKey memory key = coinV4.getPoolKey();
587
+
588
+ (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
589
+ currency,
590
+ amountIn,
591
+ address(coinV4),
592
+ minAmountOut,
593
+ key,
594
+ bytes("")
595
+ );
596
+
597
+ address trader = makeAddr("trader");
598
+
599
+ // mint some mockERC20 to the trader, so they can use it to buy the coin
600
+ mockERC20A.mint(trader, amountIn);
601
+
602
+ // have trader approve to permit2
603
+ vm.startPrank(trader);
604
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), currency, amountIn, uint48(block.timestamp + 1 days));
605
+
606
+ // Execute the swap
607
+ uint256 deadline = block.timestamp + 20;
608
+
609
+ address sender = UNIVERSAL_ROUTER;
610
+
611
+ (BalanceDelta delta, SwapParams memory swapParams, uint160 sqrtPriceX96) = _estimateSwap(commands, inputs);
612
+
613
+ address[] memory _trustedMessageSenders = new address[](1);
614
+ _trustedMessageSenders[0] = UNIVERSAL_ROUTER;
615
+
616
+ // if we want to simulate swap happening from a non-trusted message sender, we copy the router code to a new address that isn't
617
+ // trusted by the hook, and have that router execute the swap
618
+ if (!swapIsFromTrustedMessageSender) {
619
+ bytes memory routerCode = address(router).code;
620
+ address targetAddr = makeAddr("targetAddr");
621
+ vm.etch(targetAddr, routerCode);
622
+ router = IUniversalRouter(targetAddr);
623
+ // we need to approve the token again on the new router address.
624
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), currency, amountIn, uint48(block.timestamp + 1 days));
625
+ }
626
+
627
+ // if we want to simulate the trusted sender reverting, we mock the msgSender function on the router to revert
628
+ if (trustedSenderReverts) {
629
+ vm.mockCallRevert(address(router), abi.encodeWithSelector(IMsgSender.msgSender.selector), "any revert");
630
+ }
631
+
632
+ bool isCoinBuy = true;
633
+
634
+ vm.expectEmit(false, true, true, true);
635
+ emit IZoraV4CoinHook.Swapped(
636
+ sender,
637
+ trustedSenderReverts ? address(0) : trader,
638
+ swapIsFromTrustedMessageSender,
639
+ key,
640
+ CoinCommon.hashPoolKey(key),
641
+ swapParams,
642
+ delta.amount0(),
643
+ delta.amount1(),
644
+ isCoinBuy,
645
+ "",
646
+ sqrtPriceX96
647
+ );
648
+ router.execute(commands, inputs, deadline);
649
+ }
650
+
651
+ function test_getSwapPath_whenBackingCurrencyIsErc20() public {
652
+ address currency = address(mockERC20A);
653
+ _deployV4Coin(currency);
654
+
655
+ IHasSwapPath.PayoutSwapPath memory swapPath = coinV4.getPayoutSwapPath(IDeployedCoinVersionLookup(address(factory)));
656
+
657
+ assertEq(swapPath.path.length, 1);
658
+ _assertPathKeyEqual(
659
+ swapPath.path[0],
660
+ PathKey({
661
+ intermediateCurrency: Currency.wrap(address(mockERC20A)),
662
+ fee: coinV4.getPoolKey().fee,
663
+ tickSpacing: coinV4.getPoolKey().tickSpacing,
664
+ hooks: coinV4.getPoolKey().hooks,
665
+ hookData: bytes("")
666
+ }),
667
+ "path key"
668
+ );
669
+ }
670
+
671
+ function test_getSwapPath_whenBackingCurrencyProvidesPath() public {
672
+ address zora = address(mockERC20A);
673
+ ICoinV4 backingCoin = _deployV4Coin(zora);
674
+ // now create a final coin paired with the backing coin
675
+ ICoinV4 contentCoin = _deployV4Coin(address(backingCoin));
676
+
677
+ PathKey[] memory path = contentCoin.getPayoutSwapPath(IDeployedCoinVersionLookup(address(factory))).path;
678
+
679
+ // swap path should be:
680
+ // 1. content coin -> backing coin
681
+ // 2. backing coin -> zora coin
682
+ assertEq(path.length, 2);
683
+ _assertPathKeyEqual(
684
+ path[0],
685
+ PathKey({
686
+ intermediateCurrency: Currency.wrap(address(backingCoin)),
687
+ fee: contentCoin.getPoolKey().fee,
688
+ tickSpacing: contentCoin.getPoolKey().tickSpacing,
689
+ hooks: contentCoin.getPoolKey().hooks,
690
+ hookData: bytes("")
691
+ }),
692
+ "content to backing coin"
693
+ );
694
+ _assertPathKeyEqual(
695
+ path[1],
696
+ PathKey({
697
+ intermediateCurrency: Currency.wrap(zora),
698
+ fee: backingCoin.getPoolKey().fee,
699
+ tickSpacing: backingCoin.getPoolKey().tickSpacing,
700
+ hooks: backingCoin.getPoolKey().hooks,
701
+ hookData: bytes("")
702
+ }),
703
+ "backing to zora coin"
704
+ );
705
+ }
706
+
707
+ function test_swap_withBackingCoinToZora_paysRewardsInZoraOnly(uint128 amountIn) public {
708
+ // zora is a mock erc20
709
+ address zora = address(mockERC20A);
710
+ // make sure pool manager has enough zora in it so we can take the fees on the swap
711
+ mockERC20A.mint(address(poolManager), 10000000000000000 ether);
712
+
713
+ // backing coin is a mock coin that is paired with zora
714
+ ICoinV4 backingCoin = _deployV4Coin(zora);
715
+ // now create a final coin paired with the backing coin
716
+ ICoinV4 contentCoin = _deployV4Coin(address(backingCoin));
717
+
718
+ vm.assume(amountIn > 0.000000000001 ether);
719
+ vm.assume(amountIn < 10000000000000000 ether);
720
+
721
+ address trader = makeAddr("trader");
722
+ MockERC20(zora).mint(trader, amountIn);
723
+
724
+ address protocolRewardRecipient = contentCoin.protocolRewardRecipient();
725
+
726
+ // swap some zora for backing coin, so the trader has some backing coin - this should not cause a multihop swap for rewards
727
+ _swapSomeCurrencyForCoin(backingCoin, zora, uint128(IERC20(address(zora)).balanceOf(trader)), trader);
728
+
729
+ // get balances before
730
+ uint256 protocolRewardRecipientZoraBalanceBefore = IERC20(address(zora)).balanceOf(protocolRewardRecipient);
731
+ uint256 protocolRewardRecipientBackingCoinBalanceBefore = IERC20(address(backingCoin)).balanceOf(protocolRewardRecipient);
732
+ uint256 protocolRewardRecipientContentCoinBalanceBefore = IERC20(address(contentCoin)).balanceOf(protocolRewardRecipient);
733
+
734
+ // swap some backing coin for content coin, this should do final rewards transfer in correct balance
735
+ _swapSomeCurrencyForCoin(contentCoin, address(backingCoin), uint128(IERC20(address(backingCoin)).balanceOf(trader)), trader);
736
+
737
+ // swap some content coin for backing coin, this should do final rewards transfer in correct balance
738
+ _swapSomeCoinForCurrency(contentCoin, address(backingCoin), uint128(IERC20(address(contentCoin)).balanceOf(trader)), trader);
739
+
740
+ // make sure that no zora, backing, or content coin was paid out to the protocol reward recipient, but just usdc was
741
+ assertEq(IERC20(address(backingCoin)).balanceOf(protocolRewardRecipient), protocolRewardRecipientBackingCoinBalanceBefore, "backing coin was paid out");
742
+ assertEq(IERC20(address(contentCoin)).balanceOf(protocolRewardRecipient), protocolRewardRecipientContentCoinBalanceBefore, "content coin was paid out");
743
+ assertGt(IERC20(address(zora)).balanceOf(protocolRewardRecipient), protocolRewardRecipientZoraBalanceBefore, "zora was paid out");
744
+ }
745
+
746
+ function _assertPathKeyEqual(PathKey memory a, PathKey memory b, string memory keyName) internal pure {
747
+ assertEq(Currency.unwrap(a.intermediateCurrency), Currency.unwrap(b.intermediateCurrency), string.concat(keyName, " intermediateCurrency"));
748
+ assertEq(a.fee, b.fee, string.concat(keyName, " fee"));
749
+ assertEq(a.tickSpacing, b.tickSpacing, string.concat(keyName, " tickSpacing"));
750
+ assertEq(address(a.hooks), address(b.hooks), string.concat(keyName, " hooks"));
751
+ }
752
+ }