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