@zoralabs/coins 2.1.2 → 2.3.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 (107) hide show
  1. package/.turbo/turbo-build$colon$js.log +152 -0
  2. package/CHANGELOG.md +93 -0
  3. package/README.md +4 -0
  4. package/abis/BaseCoin.json +26 -5
  5. package/abis/BaseTest.json +2 -7
  6. package/abis/ContentCoin.json +26 -5
  7. package/abis/CreatorCoin.json +30 -9
  8. package/abis/FeeEstimatorHook.json +94 -6
  9. package/abis/ICoin.json +26 -0
  10. package/abis/ICoinV3.json +26 -0
  11. package/abis/ICreatorCoin.json +39 -0
  12. package/abis/IERC721.json +36 -36
  13. package/abis/IHasCoinType.json +15 -0
  14. package/abis/IHasTotalSupplyForPositions.json +15 -0
  15. package/abis/{LiquidityMigrationReceiver.json → IUpgradeableDestinationV4HookWithUpdateableFee.json} +10 -18
  16. package/abis/IZoraFactory.json +121 -0
  17. package/abis/IZoraHookRegistry.json +188 -0
  18. package/abis/VmContractHelper226.json +233 -0
  19. package/abis/ZoraFactoryImpl.json +101 -6
  20. package/abis/ZoraHookRegistry.json +375 -0
  21. package/abis/{CreatorCoinHook.json → ZoraV4CoinHook.json} +95 -2
  22. package/addresses/8453.json +6 -5
  23. package/audits/report-cantinacode-zora-0827.pdf +3498 -4
  24. package/dist/index.cjs +93 -13
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +93 -13
  27. package/dist/index.js.map +1 -1
  28. package/dist/wagmiGenerated.d.ts +144 -22
  29. package/dist/wagmiGenerated.d.ts.map +1 -1
  30. package/foundry.toml +4 -1
  31. package/package/wagmiGenerated.ts +93 -13
  32. package/package.json +6 -4
  33. package/script/PrintRegisterUpgradePath.s.sol +0 -7
  34. package/script/TestBackingCoinSwap.s.sol +0 -3
  35. package/script/TestV4Swap.s.sol +0 -3
  36. package/script/UpgradeFactoryImpl.s.sol +1 -1
  37. package/src/BaseCoin.sol +19 -24
  38. package/src/ContentCoin.sol +11 -2
  39. package/src/CreatorCoin.sol +34 -15
  40. package/src/ZoraFactoryImpl.sol +163 -92
  41. package/src/deployment/CoinsDeployerBase.sol +24 -58
  42. package/src/hook-registry/ZoraHookRegistry.sol +97 -0
  43. package/src/hooks/{BaseZoraV4CoinHook.sol → ZoraV4CoinHook.sol} +77 -15
  44. package/src/interfaces/ICoin.sol +19 -1
  45. package/src/interfaces/ICreatorCoin.sol +4 -0
  46. package/src/interfaces/IUpgradeableV4Hook.sol +18 -0
  47. package/src/interfaces/IZoraFactory.sol +51 -10
  48. package/src/interfaces/IZoraHookRegistry.sol +47 -0
  49. package/src/libs/CoinConstants.sol +43 -32
  50. package/src/libs/CoinDopplerMultiCurve.sol +11 -11
  51. package/src/libs/CoinRewardsV4.sol +68 -37
  52. package/src/libs/CoinSetup.sol +2 -9
  53. package/src/libs/DopplerMath.sol +2 -2
  54. package/src/libs/HooksDeployment.sol +13 -65
  55. package/src/libs/V4Liquidity.sol +109 -15
  56. package/src/version/ContractVersionBase.sol +1 -1
  57. package/test/Coin.t.sol +5 -5
  58. package/test/CoinRewardsV4.t.sol +33 -0
  59. package/test/CoinUniV4.t.sol +32 -30
  60. package/test/ContentCoinRewards.t.sol +363 -0
  61. package/test/CreatorCoin.t.sol +53 -29
  62. package/test/CreatorCoinRewards.t.sol +375 -0
  63. package/test/DeploymentHooks.t.sol +64 -12
  64. package/test/Factory.t.sol +24 -7
  65. package/test/HooksDeployment.t.sol +4 -4
  66. package/test/LiquidityMigration.t.sol +149 -16
  67. package/test/Upgrades.t.sol +44 -48
  68. package/test/V4Liquidity.t.sol +178 -0
  69. package/test/ZoraHookRegistry.t.sol +266 -0
  70. package/test/utils/BaseTest.sol +25 -43
  71. package/test/utils/FeeEstimatorHook.sol +4 -6
  72. package/test/utils/RewardTestHelpers.sol +106 -0
  73. package/.turbo/turbo-build.log +0 -199
  74. package/abis/AutoSwapperTest.json +0 -618
  75. package/abis/BadImpl.json +0 -15
  76. package/abis/BaseZoraV4CoinHook.json +0 -1664
  77. package/abis/CoinConstants.json +0 -158
  78. package/abis/CoinRewardsV4.json +0 -67
  79. package/abis/CoinTest.json +0 -819
  80. package/abis/CoinUniV4Test.json +0 -1128
  81. package/abis/ContentCoinHook.json +0 -1733
  82. package/abis/CreatorCoinTest.json +0 -887
  83. package/abis/Deploy.json +0 -9
  84. package/abis/DeployHooks.json +0 -9
  85. package/abis/DeployScript.json +0 -35
  86. package/abis/DeployedCoinVersionLookupTest.json +0 -740
  87. package/abis/DifferentNamespaceVersionLookup.json +0 -39
  88. package/abis/FactoryTest.json +0 -748
  89. package/abis/FakeHookNoInterface.json +0 -21
  90. package/abis/GenerateDeterministicParams.json +0 -9
  91. package/abis/HooksDeploymentTest.json +0 -645
  92. package/abis/HooksTest.json +0 -709
  93. package/abis/InvalidLiquidityMigrationReceiver.json +0 -21
  94. package/abis/LiquidityMigrationTest.json +0 -889
  95. package/abis/MockBadFactory.json +0 -15
  96. package/abis/MultiOwnableTest.json +0 -766
  97. package/abis/PrintUpgradeCommand.json +0 -9
  98. package/abis/TestDeployedCoinVersionLookupImplementation.json +0 -39
  99. package/abis/TestV4Swap.json +0 -9
  100. package/abis/UpgradeFactoryImpl.json +0 -9
  101. package/abis/UpgradeHooks.json +0 -35
  102. package/abis/UpgradesTest.json +0 -723
  103. package/src/hooks/ContentCoinHook.sol +0 -27
  104. package/src/hooks/CreatorCoinHook.sol +0 -27
  105. package/src/libs/CreatorCoinConstants.sol +0 -16
  106. package/src/libs/CreatorCoinRewards.sol +0 -34
  107. package/src/libs/MarketConstants.sol +0 -15
@@ -0,0 +1,363 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import "./utils/BaseTest.sol";
5
+ import {console} from "forge-std/console.sol";
6
+
7
+ import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
8
+ import {IHasRewardsRecipients} from "../src/interfaces/IHasRewardsRecipients.sol";
9
+ import {UniV4SwapHelper} from "../src/libs/UniV4SwapHelper.sol";
10
+ import {FeeEstimatorHook} from "./utils/FeeEstimatorHook.sol";
11
+ import {RewardTestHelpers, RewardBalances} from "./utils/RewardTestHelpers.sol";
12
+ import {CoinConstants} from "../src/libs/CoinConstants.sol";
13
+ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
14
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
15
+
16
+ contract ContentCoinRewardsTest is BaseTest {
17
+ ContentCoin internal contentCoin;
18
+ CreatorCoin internal backingCreatorCoin;
19
+
20
+ address internal platformReferrer;
21
+ address internal tradeReferrer;
22
+
23
+ function setUp() public override {
24
+ super.setUpWithBlockNumber(30267794);
25
+
26
+ deal(address(zoraToken), address(poolManager), 1_000_000_000e18);
27
+
28
+ backingCreatorCoin = CreatorCoin(_deployCreatorCoin());
29
+
30
+ vm.label(address(backingCreatorCoin), "BACKING_CREATOR_COIN");
31
+
32
+ // Set up referrer addresses for all tests
33
+ platformReferrer = makeAddr("platformReferrer");
34
+ tradeReferrer = makeAddr("tradeReferrer");
35
+ }
36
+
37
+ // Generic function to record token balances for all reward recipients
38
+ function _recordBalances(IERC20 token) internal view returns (RewardBalances memory balances) {
39
+ balances.creator = token.balanceOf(users.creator);
40
+ balances.platformReferrer = token.balanceOf(platformReferrer);
41
+ balances.tradeReferrer = token.balanceOf(tradeReferrer);
42
+ balances.protocol = token.balanceOf(contentCoin.protocolRewardRecipient());
43
+ balances.doppler = token.balanceOf(contentCoin.dopplerFeeRecipient());
44
+ }
45
+
46
+ // Helper function to record initial ZORA token balances for all reward recipients
47
+ function _recordZoraBalances() internal view returns (RewardBalances memory balances) {
48
+ return _recordBalances(zoraToken);
49
+ }
50
+
51
+ // Helper function to calculate ZORA token reward deltas after trade
52
+ function _calculateZoraRewardDeltas(RewardBalances memory initialBalances) internal view returns (RewardBalances memory deltas) {
53
+ deltas.creator = zoraToken.balanceOf(users.creator) - initialBalances.creator;
54
+ deltas.platformReferrer = zoraToken.balanceOf(platformReferrer) - initialBalances.platformReferrer;
55
+ deltas.tradeReferrer = zoraToken.balanceOf(tradeReferrer) - initialBalances.tradeReferrer;
56
+ deltas.protocol = zoraToken.balanceOf(contentCoin.protocolRewardRecipient()) - initialBalances.protocol;
57
+ deltas.doppler = zoraToken.balanceOf(contentCoin.dopplerFeeRecipient()) - initialBalances.doppler;
58
+ }
59
+
60
+ /// @dev Estimates the fees from a swap
61
+ function _estimateLpFees(bytes memory commands, bytes[] memory inputs) internal returns (FeeEstimatorHook.FeeEstimatorState memory feeState) {
62
+ uint256 snapshot = vm.snapshot();
63
+ _deployFeeEstimatorHook(address(hook));
64
+
65
+ // Execute the swap
66
+ uint256 deadline = block.timestamp + 20;
67
+ router.execute(commands, inputs, deadline);
68
+
69
+ feeState = FeeEstimatorHook(payable(address(hook))).getFeeState();
70
+
71
+ vm.revertToState(snapshot);
72
+ }
73
+
74
+ // Helper function to buy content coin
75
+ function _buyContentCoin(address currencyIn, uint128 amountIn, bool hasTradeReferrer) internal returns (uint256 feeCurrency) {
76
+ vm.warp(block.timestamp + 1 days);
77
+
78
+ vm.startPrank(users.buyer);
79
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), currencyIn, uint128(amountIn), uint48(block.timestamp + 1 days));
80
+
81
+ // Build hook data with trade referrer if provided
82
+ bytes memory hookData = hasTradeReferrer ? abi.encode(tradeReferrer) : bytes("");
83
+
84
+ (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
85
+ currencyIn,
86
+ uint128(amountIn),
87
+ address(contentCoin),
88
+ 0,
89
+ contentCoin.getPoolKey(),
90
+ hookData
91
+ );
92
+
93
+ // Estimate the total fees before executing
94
+ FeeEstimatorHook.FeeEstimatorState memory feeState = _estimateLpFees(commands, inputs);
95
+ feeCurrency = feeState.afterSwapCurrencyAmount;
96
+
97
+ router.execute(commands, inputs, block.timestamp + 1 days);
98
+ vm.stopPrank();
99
+ }
100
+
101
+ // Helper function to deploy content coin backed by creator coin
102
+ function _deployContentCoin(bool hasPlatformReferrer) internal {
103
+ // Then deploy content coin backed by the creator coin
104
+ bytes memory poolConfig = _defaultPoolConfig(address(backingCreatorCoin));
105
+
106
+ // Generate unique salt
107
+ bytes32 uniqueSalt = keccak256(abi.encodePacked("content", address(backingCreatorCoin), block.timestamp, gasleft()));
108
+
109
+ vm.prank(users.creator);
110
+ (address contentCoinAddress, ) = factory.deploy(
111
+ users.creator,
112
+ _getDefaultOwners(),
113
+ "https://content.com",
114
+ "ContentCoin",
115
+ "CONTENT",
116
+ poolConfig,
117
+ hasPlatformReferrer ? platformReferrer : address(0),
118
+ address(0), // postDeployHook
119
+ bytes(""), // postDeployHookData
120
+ uniqueSalt
121
+ );
122
+
123
+ contentCoin = ContentCoin(contentCoinAddress);
124
+ vm.label(address(contentCoin), "TEST_CONTENT_COIN");
125
+ }
126
+
127
+ // Helper function to deploy creator coin (backing for content coin)
128
+ function _deployCreatorCoin() internal returns (address) {
129
+ // Use the same multi-curve config as CreatorCoinRewards.t.sol
130
+ int24[] memory tickLower = new int24[](1);
131
+ int24[] memory tickUpper = new int24[](1);
132
+ uint16[] memory numDiscoveryPositions = new uint16[](1);
133
+ uint256[] memory maxDiscoverySupplyShare = new uint256[](1);
134
+
135
+ tickLower[0] = -138_000;
136
+ tickUpper[0] = 81_000;
137
+ numDiscoveryPositions[0] = 11;
138
+ maxDiscoverySupplyShare[0] = 0.25e18;
139
+
140
+ bytes memory poolConfig = abi.encode(
141
+ CoinConfigurationVersions.DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION,
142
+ address(zoraToken),
143
+ tickLower,
144
+ tickUpper,
145
+ numDiscoveryPositions,
146
+ maxDiscoverySupplyShare
147
+ );
148
+
149
+ // Generate unique salt
150
+ bytes32 uniqueSalt = keccak256(abi.encodePacked("creator", block.timestamp, gasleft()));
151
+
152
+ vm.prank(users.creator);
153
+ address creatorCoinAddress = factory.deployCreatorCoin(
154
+ users.creator,
155
+ _getDefaultOwners(),
156
+ "https://creator.com",
157
+ "CreatorCoin",
158
+ "CREATOR",
159
+ poolConfig,
160
+ address(0),
161
+ uniqueSalt
162
+ );
163
+
164
+ return creatorCoinAddress;
165
+ }
166
+
167
+ /// @notice Test that fee estimation matches actual reward distribution
168
+ function test_estimateAfterSwapCurrencyAmount() public {
169
+ // Deploy content coin backed by creator coin
170
+ _deployContentCoin(true);
171
+
172
+ uint128 tradeAmount = 1000 ether;
173
+
174
+ // First, get trader some backing creator coins
175
+ address trader = users.buyer;
176
+ deal(address(zoraToken), trader, tradeAmount * 2);
177
+ _swapSomeCurrencyForCoin(ICoin(address(backingCreatorCoin)), address(zoraToken), tradeAmount, trader);
178
+
179
+ // Record initial balances
180
+ RewardBalances memory initialBalances = _recordZoraBalances();
181
+
182
+ // Build swap command: Creator Coin -> Content Coin
183
+ uint128 backingBalance = uint128(backingCreatorCoin.balanceOf(trader));
184
+
185
+ vm.startPrank(trader);
186
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(backingCreatorCoin), backingBalance, uint48(block.timestamp + 1 days));
187
+
188
+ (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
189
+ address(backingCreatorCoin),
190
+ backingBalance,
191
+ address(contentCoin),
192
+ 0,
193
+ contentCoin.getPoolKey(),
194
+ bytes("") // No trade referrer
195
+ );
196
+
197
+ // Estimate fees using the same pattern as CoinUniV4.t.sol
198
+ FeeEstimatorHook.FeeEstimatorState memory feeState = _estimateLpFees(commands, inputs);
199
+
200
+ // Execute actual swap
201
+ router.execute(commands, inputs, block.timestamp + 20);
202
+ vm.stopPrank();
203
+
204
+ // Calculate actual total rewards distributed
205
+ RewardBalances memory finalRewards = _calculateZoraRewardDeltas(initialBalances);
206
+ uint256 totalActualRewards = RewardTestHelpers.getTotalRewards(finalRewards);
207
+
208
+ // Verify that total actual rewards match the estimated afterSwapCurrencyAmount
209
+ assertApproxEqRel(totalActualRewards, feeState.afterSwapCurrencyAmount, 0.25e18, "Total rewards should match estimated afterSwapCurrencyAmount");
210
+ }
211
+
212
+ /// @notice Test reward distribution with creator referrer only (no trade referrer, no platform referrer)
213
+ function test_rewards_creator_referrer_only() public {
214
+ // Deploy content coin backed by creator coin with creator referrer (inherits creator referrer)
215
+ _deployContentCoin(true);
216
+
217
+ uint128 tradeAmount = 1000 ether; // 1000 ZORA tokens
218
+
219
+ // First, trader needs to get some backing creator coins to trade for content coin
220
+ address trader = users.buyer;
221
+ deal(address(zoraToken), trader, tradeAmount * 2); // Give extra for initial swap
222
+
223
+ // Step 1: Swap ZORA for backing creator coin
224
+ _swapSomeCurrencyForCoin(ICoin(address(backingCreatorCoin)), address(zoraToken), tradeAmount, trader);
225
+
226
+ // Step 2: Record balances before content coin trade and perform the actual test trade
227
+ RewardBalances memory initialBalances = _recordZoraBalances();
228
+
229
+ // Swap backing creator coin for content coin
230
+ uint128 backingBalance = uint128(backingCreatorCoin.balanceOf(trader));
231
+ uint256 rewardsAmount = _buyContentCoin(address(backingCreatorCoin), backingBalance, false);
232
+
233
+ RewardBalances memory rewards = _calculateZoraRewardDeltas(initialBalances);
234
+
235
+ // Calculate expected rewards based on actual reward deltas (like creator coin tests do)
236
+ uint256 totalRewards = rewardsAmount;
237
+ RewardBalances memory expected = RewardTestHelpers.calculateExpectedRewards(totalRewards, true, false);
238
+ RewardTestHelpers.assertRewardsApproxEqRelWithTolerance(rewards, expected, 0.25e18);
239
+ }
240
+
241
+ /// @notice Test reward distribution with trade referrer only (no creator referrer, no platform referrer)
242
+ function test_rewards_trade_referrer_only() public {
243
+ _deployContentCoin(false); // Deploy without platform referrer
244
+
245
+ uint128 tradeAmount = 1000 ether;
246
+ address trader = users.buyer;
247
+ deal(address(zoraToken), trader, tradeAmount * 2);
248
+
249
+ // Step 1: Get backing creator coins
250
+ _swapSomeCurrencyForCoin(ICoin(address(backingCreatorCoin)), address(zoraToken), tradeAmount, trader);
251
+
252
+ // Step 2: Test content coin trade
253
+ RewardBalances memory initialBalances = _recordZoraBalances();
254
+ uint128 backingBalance = uint128(backingCreatorCoin.balanceOf(trader));
255
+ uint256 rewardsAmount = _buyContentCoin(address(backingCreatorCoin), backingBalance, true);
256
+ RewardBalances memory rewards = _calculateZoraRewardDeltas(initialBalances);
257
+
258
+ // Step 3: Validate rewards
259
+ RewardBalances memory expected = RewardTestHelpers.calculateExpectedRewards(rewardsAmount, false, true);
260
+ RewardTestHelpers.assertRewardsApproxEqRelWithTolerance(rewards, expected, 0.25e18);
261
+ }
262
+
263
+ /// @notice Test reward distribution with creator referrer + trade referrer (no platform referrer)
264
+ function test_rewards_platform_and_trade_referrers() public {
265
+ _deployContentCoin(true); // Deploy with platform referrer
266
+
267
+ uint128 tradeAmount = 1000 ether;
268
+ address trader = users.buyer;
269
+ deal(address(zoraToken), trader, tradeAmount * 2);
270
+
271
+ // Step 1: Get backing creator coins
272
+ _swapSomeCurrencyForCoin(ICoin(address(backingCreatorCoin)), address(zoraToken), tradeAmount, trader);
273
+
274
+ // Step 2: Test content coin trade
275
+ RewardBalances memory initialBalances = _recordZoraBalances();
276
+ uint128 backingBalance = uint128(backingCreatorCoin.balanceOf(trader));
277
+ uint256 rewardsAmount = _buyContentCoin(address(backingCreatorCoin), backingBalance, true);
278
+ RewardBalances memory rewards = _calculateZoraRewardDeltas(initialBalances);
279
+
280
+ // Step 3: Validate rewards
281
+ RewardBalances memory expected = RewardTestHelpers.calculateExpectedRewards(rewardsAmount, true, true);
282
+ console.log("protocol rewards", rewards.protocol);
283
+ console.log("expected protocol rewards", expected.protocol);
284
+ RewardTestHelpers.assertRewardsApproxEqRelWithTolerance(rewards, expected, 0.25e18);
285
+ }
286
+
287
+ /// @notice Test reward distribution with no referrers (all address(0))
288
+ function test_rewards_no_referrers() public {
289
+ _deployContentCoin(false); // Deploy without platform referrer
290
+
291
+ uint128 tradeAmount = 1000 ether;
292
+ address trader = users.buyer;
293
+ deal(address(zoraToken), trader, tradeAmount * 2);
294
+
295
+ // Step 1: Get backing creator coins
296
+ _swapSomeCurrencyForCoin(ICoin(address(backingCreatorCoin)), address(zoraToken), tradeAmount, trader);
297
+
298
+ // Step 2: Test content coin trade
299
+ RewardBalances memory initialBalances = _recordZoraBalances();
300
+ uint128 backingBalance = uint128(backingCreatorCoin.balanceOf(trader));
301
+ uint256 rewardsAmount = _buyContentCoin(address(backingCreatorCoin), backingBalance, false);
302
+ RewardBalances memory rewards = _calculateZoraRewardDeltas(initialBalances);
303
+
304
+ // Step 3: Validate rewards
305
+ RewardBalances memory expected = RewardTestHelpers.calculateExpectedRewards(rewardsAmount, false, false);
306
+ RewardTestHelpers.assertRewardsApproxEqRelWithTolerance(rewards, expected, 0.25e18);
307
+ }
308
+
309
+ function test_isNotLegacyCreatorCoinCategorization() public {
310
+ vm.createSelectFork("base", 31835069);
311
+
312
+ // Use the same content coin from the upgrades test
313
+ address contentCoinAddress = 0x4E93A01c90f812284F71291a8d1415a904957156;
314
+
315
+ // Test that the content coin is NOT categorized as a legacy creator coin
316
+ bool isLegacy = CoinRewardsV4.isLegacyCreatorCoin(IHasRewardsRecipients(contentCoinAddress));
317
+
318
+ assertFalse(isLegacy, "Content coin should NOT be categorized as legacy creator coin");
319
+ }
320
+
321
+ /// @notice Test reward distribution when platform referrer rejects ETH - should fallback to protocol recipient
322
+ function test_rewards_platform_referrer_eth_rejection_fallback() public {
323
+ // Deploy ETH-rejecting contract to use as platform referrer
324
+ EthRejectingContract ethRejecter = new EthRejectingContract();
325
+
326
+ // Deploy ETH-backed coin (like test_distributesMarketRewardsInEth)
327
+ address currency = address(0); // ETH backing
328
+ bytes32 salt = keccak256(abi.encodePacked("eth-reject-test"));
329
+ _deployV4Coin(currency, address(ethRejecter), salt); // ethRejecter becomes platform referrer
330
+
331
+ // Fund trader with ETH
332
+ uint128 ethAmount = 0.1 ether;
333
+ address trader = makeAddr("trader");
334
+ deal(trader, ethAmount);
335
+
336
+ // Record initial ETH balances
337
+ uint256 initialProtocolEth = coinV4.protocolRewardRecipient().balance;
338
+ uint256 initialRejecterEth = address(ethRejecter).balance;
339
+
340
+ // Execute ETH -> Coin trade using the working BaseTest function
341
+ _swapSomeCurrencyForCoin(coinV4, currency, ethAmount, trader);
342
+
343
+ // Calculate ETH balance deltas
344
+ uint256 protocolEthDelta = coinV4.protocolRewardRecipient().balance - initialProtocolEth;
345
+ uint256 rejecterEthDelta = address(ethRejecter).balance - initialRejecterEth;
346
+
347
+ // Verify ETH-rejecting contract got no ETH
348
+ assertEq(rejecterEthDelta, 0, "Platform referrer should receive no ETH");
349
+
350
+ // Verify protocol got ETH (backup mechanism worked)
351
+ assertGt(protocolEthDelta, 0, "Protocol should receive backup ETH from failed platform referrer");
352
+ }
353
+ }
354
+
355
+ // Contract that rejects ETH transfers (no payable functions)
356
+ contract EthRejectingContract {
357
+ // This contract has no receive() or fallback() payable functions
358
+ // so ETH transfers will fail
359
+ receive() external payable {
360
+ console.log("EthRejectingContract received ETH");
361
+ revert("EthRejectingContract received ETH");
362
+ }
363
+ }
@@ -5,7 +5,7 @@ import "./utils/BaseTest.sol";
5
5
 
6
6
  import {ICreatorCoin} from "../src/interfaces/ICreatorCoin.sol";
7
7
  import {ICreatorCoinHook} from "../src/interfaces/ICreatorCoinHook.sol";
8
- import {CreatorCoinConstants} from "../src/libs/CreatorCoinConstants.sol";
8
+ import {CoinConstants} from "../src/libs/CoinConstants.sol";
9
9
  import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
10
10
  import {UniV4SwapHelper} from "../src/libs/UniV4SwapHelper.sol";
11
11
 
@@ -65,11 +65,11 @@ contract CreatorCoinTest is BaseTest {
65
65
  assertEq(creatorCoin.name(), "Testcoin");
66
66
  assertEq(creatorCoin.symbol(), "TEST");
67
67
  assertEq(creatorCoin.payoutRecipient(), users.creator);
68
- assertEq(creatorCoin.currency(), CreatorCoinConstants.CURRENCY);
69
- assertEq(creatorCoin.totalSupply(), CreatorCoinConstants.TOTAL_SUPPLY);
68
+ assertEq(creatorCoin.currency(), CoinConstants.CREATOR_COIN_CURRENCY);
69
+ assertEq(creatorCoin.totalSupply(), CoinConstants.TOTAL_SUPPLY);
70
70
 
71
- assertEq(creatorCoin.balanceOf(address(creatorCoin)), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
72
- assertEq(creatorCoin.balanceOf(address(creatorCoin.poolManager())), CreatorCoinConstants.MARKET_SUPPLY);
71
+ assertEq(creatorCoin.balanceOf(address(creatorCoin)), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
72
+ assertEq(creatorCoin.balanceOf(address(creatorCoin.poolManager())), CoinConstants.CREATOR_COIN_MARKET_SUPPLY);
73
73
  }
74
74
 
75
75
  function test_deploy_creator_coin_with_invalid_currency_reverts() public {
@@ -105,7 +105,7 @@ contract CreatorCoinTest is BaseTest {
105
105
  uint256 deploymentTime = block.timestamp;
106
106
 
107
107
  assertEq(creatorCoin.vestingStartTime(), deploymentTime);
108
- assertEq(creatorCoin.vestingEndTime(), deploymentTime + CreatorCoinConstants.CREATOR_VESTING_DURATION);
108
+ assertEq(creatorCoin.vestingEndTime(), deploymentTime + CoinConstants.CREATOR_VESTING_DURATION);
109
109
  assertEq(creatorCoin.totalClaimed(), 0);
110
110
  }
111
111
 
@@ -124,16 +124,16 @@ contract CreatorCoinTest is BaseTest {
124
124
  vm.warp(creatorCoin.vestingStartTime() + oneYear);
125
125
 
126
126
  // After 1 year out of 5, should be able to claim 20% of vesting supply
127
- uint256 expectedClaimable = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * oneYear) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
127
+ uint256 expectedClaimable = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
128
128
  assertEq(creatorCoin.getClaimableAmount(), expectedClaimable);
129
129
  }
130
130
 
131
131
  function test_getClaimableAmount_after_half_vesting_period() public {
132
- uint256 halfVesting = CreatorCoinConstants.CREATOR_VESTING_DURATION / 2;
132
+ uint256 halfVesting = CoinConstants.CREATOR_VESTING_DURATION / 2;
133
133
  vm.warp(creatorCoin.vestingStartTime() + halfVesting);
134
134
 
135
135
  // After 2.5 years, should be able to claim 50% of vesting supply
136
- uint256 expectedClaimable = CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 2;
136
+ uint256 expectedClaimable = CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 2;
137
137
  assertEq(creatorCoin.getClaimableAmount(), expectedClaimable);
138
138
  }
139
139
 
@@ -141,14 +141,14 @@ contract CreatorCoinTest is BaseTest {
141
141
  vm.warp(creatorCoin.vestingEndTime());
142
142
 
143
143
  // After full vesting period, should be able to claim entire vesting supply
144
- assertEq(creatorCoin.getClaimableAmount(), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
144
+ assertEq(creatorCoin.getClaimableAmount(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
145
145
  }
146
146
 
147
147
  function test_getClaimableAmount_after_vesting_period_ends() public {
148
148
  vm.warp(creatorCoin.vestingEndTime() + 365 days);
149
149
 
150
150
  // Even after vesting ends, should still be able to claim entire vesting supply
151
- assertEq(creatorCoin.getClaimableAmount(), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
151
+ assertEq(creatorCoin.getClaimableAmount(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
152
152
  }
153
153
 
154
154
  function test_getClaimableAmount_after_one_day() public {
@@ -156,12 +156,12 @@ contract CreatorCoinTest is BaseTest {
156
156
  uint256 oneDay = 1 days;
157
157
  vm.warp(creatorCoin.vestingStartTime() + oneDay);
158
158
 
159
- uint256 expectedClaimable = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * oneDay) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
159
+ uint256 expectedClaimable = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneDay) / CoinConstants.CREATOR_VESTING_DURATION;
160
160
  assertEq(creatorCoin.getClaimableAmount(), expectedClaimable);
161
161
 
162
162
  // Verify it's a small but non-zero amount
163
163
  assertGt(expectedClaimable, 0);
164
- assertLt(expectedClaimable, CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 1000); // Less than 0.1%
164
+ assertLt(expectedClaimable, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 1000); // Less than 0.1%
165
165
  }
166
166
 
167
167
  function test_claimVesting_at_launch() public {
@@ -175,7 +175,7 @@ contract CreatorCoinTest is BaseTest {
175
175
  uint256 oneYear = 365 days;
176
176
  vm.warp(creatorCoin.vestingStartTime() + oneYear);
177
177
 
178
- uint256 expectedClaimable = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * oneYear) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
178
+ uint256 expectedClaimable = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
179
179
  uint256 initialCreatorBalance = creatorCoin.balanceOf(users.creator);
180
180
  uint256 initialContractBalance = creatorCoin.balanceOf(address(creatorCoin));
181
181
 
@@ -201,7 +201,7 @@ contract CreatorCoinTest is BaseTest {
201
201
 
202
202
  // First claim after 1 year
203
203
  vm.warp(creatorCoin.vestingStartTime() + oneYear);
204
- uint256 expectedClaim1 = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * oneYear) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
204
+ uint256 expectedClaim1 = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
205
205
  uint256 claimed1 = creatorCoin.claimVesting();
206
206
 
207
207
  assertEq(claimed1, expectedClaim1);
@@ -210,7 +210,7 @@ contract CreatorCoinTest is BaseTest {
210
210
 
211
211
  // Second claim after another year (2 years total)
212
212
  vm.warp(creatorCoin.vestingStartTime() + 2 * oneYear);
213
- uint256 totalVestedAfter2Years = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * 2 * oneYear) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
213
+ uint256 totalVestedAfter2Years = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * 2 * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
214
214
  uint256 expectedClaim2 = totalVestedAfter2Years - expectedClaim1;
215
215
 
216
216
  uint256 claimed2 = creatorCoin.claimVesting();
@@ -241,9 +241,9 @@ contract CreatorCoinTest is BaseTest {
241
241
 
242
242
  uint256 claimedAmount = creatorCoin.claimVesting();
243
243
 
244
- assertEq(claimedAmount, CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
245
- assertEq(creatorCoin.totalClaimed(), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
246
- assertEq(creatorCoin.balanceOf(users.creator), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
244
+ assertEq(claimedAmount, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
245
+ assertEq(creatorCoin.totalClaimed(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
246
+ assertEq(creatorCoin.balanceOf(users.creator), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
247
247
 
248
248
  // Subsequent claims should return 0
249
249
  uint256 secondClaim = creatorCoin.claimVesting();
@@ -251,21 +251,21 @@ contract CreatorCoinTest is BaseTest {
251
251
  }
252
252
 
253
253
  function test_claimVesting_partial_then_full() public {
254
- uint256 halfVesting = CreatorCoinConstants.CREATOR_VESTING_DURATION / 2;
254
+ uint256 halfVesting = CoinConstants.CREATOR_VESTING_DURATION / 2;
255
255
 
256
256
  // Claim half way through vesting
257
257
  vm.warp(creatorCoin.vestingStartTime() + halfVesting);
258
258
  uint256 partialClaim = creatorCoin.claimVesting();
259
- assertEq(partialClaim, CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 2);
259
+ assertEq(partialClaim, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 2);
260
260
 
261
261
  // Claim the rest after full vesting
262
262
  vm.warp(creatorCoin.vestingEndTime());
263
263
  uint256 remainingClaim = creatorCoin.claimVesting();
264
- assertEq(remainingClaim, CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 2);
264
+ assertEq(remainingClaim, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 2);
265
265
 
266
266
  // Total should equal full vesting supply
267
- assertEq(creatorCoin.totalClaimed(), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
268
- assertEq(creatorCoin.balanceOf(users.creator), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
267
+ assertEq(creatorCoin.totalClaimed(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
268
+ assertEq(creatorCoin.balanceOf(users.creator), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
269
269
  }
270
270
 
271
271
  function test_vesting_calculation_edge_cases() public {
@@ -277,17 +277,17 @@ contract CreatorCoinTest is BaseTest {
277
277
  vm.warp(creatorCoin.vestingStartTime() + 1);
278
278
  uint256 claimableAfterOneSecond = creatorCoin.getClaimableAmount();
279
279
  assertGt(claimableAfterOneSecond, 0);
280
- assertLt(claimableAfterOneSecond, CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 1000000); // Very small amount
280
+ assertLt(claimableAfterOneSecond, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 1000000); // Very small amount
281
281
 
282
282
  // Test one second before vesting ends
283
283
  vm.warp(creatorCoin.vestingEndTime() - 1);
284
284
  uint256 claimableBeforeEnd = creatorCoin.getClaimableAmount();
285
- assertLt(claimableBeforeEnd, CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
286
- assertGt(claimableBeforeEnd, CreatorCoinConstants.CREATOR_VESTING_SUPPLY - (CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 1000000));
285
+ assertLt(claimableBeforeEnd, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
286
+ assertGt(claimableBeforeEnd, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY - (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 1000000));
287
287
 
288
288
  // Test at exact vesting end time
289
289
  vm.warp(creatorCoin.vestingEndTime());
290
- assertEq(creatorCoin.getClaimableAmount(), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
290
+ assertEq(creatorCoin.getClaimableAmount(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
291
291
  }
292
292
 
293
293
  function test_vesting_frequent_small_claims() public {
@@ -302,11 +302,35 @@ contract CreatorCoinTest is BaseTest {
302
302
  }
303
303
 
304
304
  // Verify total claimed matches expected amount for 7 days
305
- uint256 expectedTotal = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * 7 days) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
305
+ uint256 expectedTotal = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * 7 days) / CoinConstants.CREATOR_VESTING_DURATION;
306
306
  assertEq(totalClaimed, expectedTotal);
307
307
  assertEq(creatorCoin.totalClaimed(), expectedTotal);
308
308
  }
309
309
 
310
+ function test_vesting_duration_accounts_for_leap_years() public pure {
311
+ // Verify the vesting duration is exactly 5 years accounting for leap years
312
+ // 365.25 days per year * 5 years = 1826.25 days = 157,788,000 seconds
313
+ uint256 expectedDuration = 5 * 365.25 days;
314
+ uint256 expectedSeconds = 157_788_000; // 5 * 365.25 * 24 * 60 * 60
315
+
316
+ assertEq(CoinConstants.CREATOR_VESTING_DURATION, expectedDuration);
317
+ assertEq(CoinConstants.CREATOR_VESTING_DURATION, expectedSeconds);
318
+
319
+ // Verify it's longer than 5 * 365 days (which would be the old incorrect duration)
320
+ uint256 oldIncorrectDuration = 5 * 365 days;
321
+ uint256 differenceInSeconds = expectedDuration - oldIncorrectDuration;
322
+ uint256 expectedDifferenceInDays = 1.25 days; // 1.25 days = 108,000 seconds
323
+
324
+ assertEq(differenceInSeconds, expectedDifferenceInDays);
325
+ assertEq(differenceInSeconds, 108_000); // 1.25 * 24 * 60 * 60
326
+
327
+ // Verify this matches exactly 5 years with leap year correction
328
+ // Over 5 years, there's typically 1 leap day (Feb 29), plus 0.25 day per year
329
+ // for the quarter-day that accumulates: 1 + (5 * 0.25) = 2.25 days total
330
+ // But we use 365.25 average, so: 5 * 0.25 = 1.25 additional days
331
+ assertTrue(CoinConstants.CREATOR_VESTING_DURATION > oldIncorrectDuration);
332
+ }
333
+
310
334
  function test_buy(uint128 amountIn) public {
311
335
  vm.assume(amountIn > 0.00001e18);
312
336
  vm.assume(amountIn < 500_000e18);