@zoralabs/coins 2.5.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build$colon$js.log +136 -130
- package/CHANGELOG.md +28 -17
- package/abis/BaseCoin.json +5 -0
- package/abis/ContentCoin.json +5 -0
- package/abis/ICoin.json +5 -0
- package/abis/ICoinV3.json +5 -0
- package/abis/ITrendCoin.json +140 -0
- package/abis/ITrendCoinErrors.json +33 -0
- package/abis/IUniversalRouter.json +61 -0
- package/abis/IZoraFactory.json +237 -0
- package/abis/TrendCoin.json +2053 -0
- package/abis/ZoraFactoryImpl.json +242 -0
- package/dist/index.cjs +955 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +953 -138
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +1388 -149
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/foundry.toml +1 -0
- package/package/wagmiGenerated.ts +962 -139
- package/package.json +2 -2
- package/src/BaseCoin.sol +12 -12
- package/src/ContentCoin.sol +20 -1
- package/src/CreatorCoin.sol +3 -0
- package/src/TrendCoin.sol +117 -0
- package/src/ZoraFactoryImpl.sol +142 -1
- package/src/hooks/ZoraV4CoinHook.sol +17 -7
- package/src/interfaces/ICoin.sol +5 -1
- package/src/interfaces/ICreatorCoin.sol +0 -3
- package/src/interfaces/IPoolManager.sol +13 -0
- package/src/interfaces/ITrendCoin.sol +26 -0
- package/src/interfaces/ITrendCoinErrors.sol +24 -0
- package/src/interfaces/IZoraFactory.sol +60 -1
- package/src/libs/CoinConstants.sol +13 -1
- package/src/libs/CoinRewardsV4.sol +82 -21
- package/src/libs/TickerUtils.sol +66 -0
- package/src/libs/UniV4SwapToCurrency.sol +2 -1
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/CoinRewardsV4.t.sol +48 -0
- package/test/CreatorCoin.t.sol +2 -1
- package/test/Factory.t.sol +31 -5
- package/test/LaunchFee.t.sol +0 -2
- package/test/LiquidityMigration.t.sol +0 -2
- package/test/TrendCoin.t.sol +1128 -0
- package/test/Upgrades.t.sol +16 -3
- package/test/utils/FeeEstimatorHook.sol +36 -10
- package/test/utils/V4TestSetup.sol +36 -4
- package/wagmi.config.ts +2 -0
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import {BaseTest} from "./utils/BaseTest.sol";
|
|
5
|
+
import {console} from "forge-std/console.sol";
|
|
6
|
+
|
|
7
|
+
import {IZoraFactory} from "../src/interfaces/IZoraFactory.sol";
|
|
8
|
+
import {IHasRewardsRecipients} from "../src/interfaces/IHasRewardsRecipients.sol";
|
|
9
|
+
import {IHasCoinType} from "../src/interfaces/ICoin.sol";
|
|
10
|
+
import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
|
|
11
|
+
import {UniV4SwapHelper} from "../src/libs/UniV4SwapHelper.sol";
|
|
12
|
+
import {FeeEstimatorHook} from "./utils/FeeEstimatorHook.sol";
|
|
13
|
+
import {RewardTestHelpers, RewardBalances} from "./utils/RewardTestHelpers.sol";
|
|
14
|
+
import {CoinConstants} from "../src/libs/CoinConstants.sol";
|
|
15
|
+
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
16
|
+
import {TrendCoin} from "../src/TrendCoin.sol";
|
|
17
|
+
import {ITrendCoin} from "../src/interfaces/ITrendCoin.sol";
|
|
18
|
+
import {ITrendCoinErrors} from "../src/interfaces/ITrendCoinErrors.sol";
|
|
19
|
+
import {ICoin} from "../src/interfaces/ICoin.sol";
|
|
20
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
21
|
+
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
|
22
|
+
import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
|
|
23
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
24
|
+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
25
|
+
import {ZoraFactoryImpl} from "../src/ZoraFactoryImpl.sol";
|
|
26
|
+
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
|
27
|
+
import {ProxyShim} from "../src/utils/ProxyShim.sol";
|
|
28
|
+
import {ZoraFactory} from "../src/proxy/ZoraFactory.sol";
|
|
29
|
+
import {ZoraHookRegistry} from "../src/hook-registry/ZoraHookRegistry.sol";
|
|
30
|
+
import {PoolConfiguration} from "../src/types/PoolConfiguration.sol";
|
|
31
|
+
|
|
32
|
+
contract TrendCoinTest is BaseTest {
|
|
33
|
+
TrendCoin internal trendCoin;
|
|
34
|
+
|
|
35
|
+
function setUp() public override {
|
|
36
|
+
super.setUpNonForked();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============ Factory Deployment Tests ============
|
|
40
|
+
|
|
41
|
+
function test_deployTrendCoin_basic() public {
|
|
42
|
+
string memory symbol = "TESTTREND";
|
|
43
|
+
|
|
44
|
+
(address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
|
|
45
|
+
|
|
46
|
+
trendCoin = TrendCoin(coinAddress);
|
|
47
|
+
|
|
48
|
+
// Verify basic properties
|
|
49
|
+
assertEq(trendCoin.symbol(), symbol, "Symbol should match");
|
|
50
|
+
assertEq(trendCoin.name(), symbol, "Name should equal symbol for trend coins");
|
|
51
|
+
assertEq(uint8(trendCoin.coinType()), uint8(IHasCoinType.CoinType.Trend), "Should be Trend coin type");
|
|
52
|
+
|
|
53
|
+
// TrendCoins have no payout recipient or platform referrer
|
|
54
|
+
assertEq(trendCoin.payoutRecipient(), address(0), "Payout recipient should be zero");
|
|
55
|
+
assertEq(trendCoin.platformReferrer(), address(0), "Platform referrer should be zero");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function test_deployTrendCoin_emitsEventWithPoolConfig() public {
|
|
59
|
+
string memory symbol = "EVENTTEST";
|
|
60
|
+
|
|
61
|
+
// Get expected pool config
|
|
62
|
+
bytes memory expectedPoolConfig = CoinConfigurationVersions.defaultConfig(CoinConstants.CREATOR_COIN_CURRENCY);
|
|
63
|
+
|
|
64
|
+
vm.expectEmit(true, false, false, false);
|
|
65
|
+
emit IZoraFactory.TrendCoinCreated(
|
|
66
|
+
address(this),
|
|
67
|
+
symbol,
|
|
68
|
+
address(0),
|
|
69
|
+
PoolKey(Currency.wrap(address(0)), Currency.wrap(address(0)), 0, 0, hook),
|
|
70
|
+
bytes32(0),
|
|
71
|
+
expectedPoolConfig,
|
|
72
|
+
""
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
(address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
|
|
76
|
+
|
|
77
|
+
// Verify coin was created
|
|
78
|
+
assertTrue(coinAddress != address(0), "Coin should be created");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function test_deployTrendCoin_addressCanBePredicted() public {
|
|
82
|
+
string memory symbol = "PREDICT";
|
|
83
|
+
|
|
84
|
+
// Get predicted address before deployment
|
|
85
|
+
address predictedAddress = factory.trendCoinAddress(symbol);
|
|
86
|
+
|
|
87
|
+
// Deploy the coin
|
|
88
|
+
(address actualAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
|
|
89
|
+
|
|
90
|
+
// Verify prediction matches
|
|
91
|
+
assertEq(actualAddress, predictedAddress, "Predicted address should match actual");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function test_deployTrendCoin_tickerUniqueness() public {
|
|
95
|
+
string memory symbol = "UNIQUE";
|
|
96
|
+
|
|
97
|
+
// Deploy first coin
|
|
98
|
+
factory.deployTrendCoin(symbol, address(0), "");
|
|
99
|
+
|
|
100
|
+
// Try to deploy with same ticker - should revert
|
|
101
|
+
vm.expectRevert(abi.encodeWithSelector(ITrendCoinErrors.TickerAlreadyUsed.selector, symbol));
|
|
102
|
+
factory.deployTrendCoin(symbol, address(0), "");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function test_deployTrendCoin_tickerCaseInsensitive() public {
|
|
106
|
+
// Deploy with lowercase
|
|
107
|
+
factory.deployTrendCoin("test", address(0), "");
|
|
108
|
+
|
|
109
|
+
// Try to deploy with uppercase - should revert (same ticker, different case)
|
|
110
|
+
vm.expectRevert(abi.encodeWithSelector(ITrendCoinErrors.TickerAlreadyUsed.selector, "TEST"));
|
|
111
|
+
factory.deployTrendCoin("TEST", address(0), "");
|
|
112
|
+
|
|
113
|
+
// Try with mixed case - should also revert
|
|
114
|
+
vm.expectRevert(abi.encodeWithSelector(ITrendCoinErrors.TickerAlreadyUsed.selector, "TeSt"));
|
|
115
|
+
factory.deployTrendCoin("TeSt", address(0), "");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function test_deployTrendCoin_differentTickersAllowed() public {
|
|
119
|
+
// Deploy multiple coins with different tickers
|
|
120
|
+
(address coin1, ) = factory.deployTrendCoin("TICKER1", address(0), "");
|
|
121
|
+
(address coin2, ) = factory.deployTrendCoin("TICKER2", address(0), "");
|
|
122
|
+
(address coin3, ) = factory.deployTrendCoin("TICKER3", address(0), "");
|
|
123
|
+
|
|
124
|
+
// All should be different addresses
|
|
125
|
+
assertTrue(coin1 != coin2, "Coin1 and Coin2 should have different addresses");
|
|
126
|
+
assertTrue(coin2 != coin3, "Coin2 and Coin3 should have different addresses");
|
|
127
|
+
assertTrue(coin1 != coin3, "Coin1 and Coin3 should have different addresses");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function test_deployTrendCoin_fullSupplyToPool() public {
|
|
131
|
+
string memory symbol = "FULLPOOL";
|
|
132
|
+
|
|
133
|
+
(address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
|
|
134
|
+
trendCoin = TrendCoin(coinAddress);
|
|
135
|
+
|
|
136
|
+
// Total supply should be the full 1B
|
|
137
|
+
assertEq(trendCoin.totalSupply(), CoinConstants.TOTAL_SUPPLY, "Total supply should be 1B");
|
|
138
|
+
|
|
139
|
+
// Verify token allocation - the coin itself should have 0 balance (all sent to hook for pool)
|
|
140
|
+
assertEq(trendCoin.balanceOf(coinAddress), 0, "Coin should have no balance");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============ Symbol Validation Tests ============
|
|
144
|
+
|
|
145
|
+
function test_deployTrendCoin_validSymbols_lettersOnly() public {
|
|
146
|
+
// Test uppercase letters
|
|
147
|
+
(address coin1, ) = factory.deployTrendCoin("ABC", address(0), "");
|
|
148
|
+
assertTrue(coin1 != address(0), "Coin with uppercase letters should deploy");
|
|
149
|
+
|
|
150
|
+
// Test lowercase letters
|
|
151
|
+
(address coin2, ) = factory.deployTrendCoin("xyz", address(0), "");
|
|
152
|
+
assertTrue(coin2 != address(0), "Coin with lowercase letters should deploy");
|
|
153
|
+
|
|
154
|
+
// Test mixed case
|
|
155
|
+
(address coin3, ) = factory.deployTrendCoin("TestCoin", address(0), "");
|
|
156
|
+
assertTrue(coin3 != address(0), "Coin with mixed case letters should deploy");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function test_deployTrendCoin_validSymbols_numbersOnly() public {
|
|
160
|
+
(address coin1, ) = factory.deployTrendCoin("123", address(0), "");
|
|
161
|
+
assertTrue(coin1 != address(0), "Coin with numbers should deploy");
|
|
162
|
+
|
|
163
|
+
(address coin2, ) = factory.deployTrendCoin("456", address(0), "");
|
|
164
|
+
assertTrue(coin2 != address(0), "Coin with different numbers should deploy");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function test_deployTrendCoin_invalidSymbols_dashOnly() public {
|
|
168
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
169
|
+
factory.deployTrendCoin("--", address(0), "");
|
|
170
|
+
|
|
171
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
172
|
+
factory.deployTrendCoin("---", address(0), "");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function test_deployTrendCoin_invalidSymbols_spaceOnly() public {
|
|
176
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
177
|
+
factory.deployTrendCoin(" ", address(0), "");
|
|
178
|
+
|
|
179
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
180
|
+
factory.deployTrendCoin(" ", address(0), "");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function test_deployTrendCoin_validSymbols_allAllowedCharacters() public {
|
|
184
|
+
(address coin1, ) = factory.deployTrendCoin("ABC123xyz", address(0), "");
|
|
185
|
+
assertTrue(coin1 != address(0), "Coin with all character types should deploy");
|
|
186
|
+
|
|
187
|
+
(address coin2, ) = factory.deployTrendCoin("Test123Coin", address(0), "");
|
|
188
|
+
assertTrue(coin2 != address(0), "Coin with mixed valid characters should deploy");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
struct InvalidSymbolTestCase {
|
|
192
|
+
string symbol;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function fixtureInvalidSymbols() public pure returns (InvalidSymbolTestCase[] memory) {
|
|
196
|
+
InvalidSymbolTestCase[] memory cases = new InvalidSymbolTestCase[](35);
|
|
197
|
+
cases[0] = InvalidSymbolTestCase("TEST!");
|
|
198
|
+
cases[1] = InvalidSymbolTestCase("TEST@");
|
|
199
|
+
cases[2] = InvalidSymbolTestCase("TEST#");
|
|
200
|
+
cases[3] = InvalidSymbolTestCase("TEST$");
|
|
201
|
+
cases[4] = InvalidSymbolTestCase("TEST%");
|
|
202
|
+
cases[5] = InvalidSymbolTestCase("TEST^");
|
|
203
|
+
cases[6] = InvalidSymbolTestCase("TEST&");
|
|
204
|
+
cases[7] = InvalidSymbolTestCase("TEST*");
|
|
205
|
+
cases[8] = InvalidSymbolTestCase("TEST(");
|
|
206
|
+
cases[9] = InvalidSymbolTestCase("TEST)");
|
|
207
|
+
cases[10] = InvalidSymbolTestCase("TEST_");
|
|
208
|
+
cases[11] = InvalidSymbolTestCase("TEST+");
|
|
209
|
+
cases[12] = InvalidSymbolTestCase("TEST=");
|
|
210
|
+
cases[13] = InvalidSymbolTestCase("TEST[");
|
|
211
|
+
cases[14] = InvalidSymbolTestCase("TEST]");
|
|
212
|
+
cases[15] = InvalidSymbolTestCase("TEST{");
|
|
213
|
+
cases[16] = InvalidSymbolTestCase("TEST}");
|
|
214
|
+
cases[17] = InvalidSymbolTestCase("TEST|");
|
|
215
|
+
cases[18] = InvalidSymbolTestCase("TEST\\");
|
|
216
|
+
cases[19] = InvalidSymbolTestCase("TEST:");
|
|
217
|
+
cases[20] = InvalidSymbolTestCase("TEST;");
|
|
218
|
+
cases[21] = InvalidSymbolTestCase('TEST"');
|
|
219
|
+
cases[22] = InvalidSymbolTestCase("TEST'");
|
|
220
|
+
cases[23] = InvalidSymbolTestCase("TEST<");
|
|
221
|
+
cases[24] = InvalidSymbolTestCase("TEST>");
|
|
222
|
+
cases[25] = InvalidSymbolTestCase("TEST,");
|
|
223
|
+
cases[26] = InvalidSymbolTestCase("TEST.");
|
|
224
|
+
cases[27] = InvalidSymbolTestCase("TEST?");
|
|
225
|
+
cases[28] = InvalidSymbolTestCase("TEST/");
|
|
226
|
+
cases[29] = InvalidSymbolTestCase("TEST~");
|
|
227
|
+
cases[30] = InvalidSymbolTestCase("TEST_COIN");
|
|
228
|
+
cases[31] = InvalidSymbolTestCase("TEST.COIN");
|
|
229
|
+
cases[32] = InvalidSymbolTestCase("TEST!@#");
|
|
230
|
+
cases[33] = InvalidSymbolTestCase("TEST COIN"); // space not allowed
|
|
231
|
+
cases[34] = InvalidSymbolTestCase("TEST-COIN"); // dash not allowed
|
|
232
|
+
return cases;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function tableInvalidSymbolsTest(InvalidSymbolTestCase memory invalidSymbols) public {
|
|
236
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
237
|
+
factory.deployTrendCoin(invalidSymbols.symbol, address(0), "");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function test_deployTrendCoin_invalidSymbols_withSpaces() public {
|
|
241
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
242
|
+
factory.deployTrendCoin("TEST COIN", address(0), "");
|
|
243
|
+
|
|
244
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
245
|
+
factory.deployTrendCoin(" TEST ", address(0), "");
|
|
246
|
+
|
|
247
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
248
|
+
factory.deployTrendCoin("TEST COIN 123", address(0), "");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function test_deployTrendCoin_validSymbols_lettersAndNumbers() public {
|
|
252
|
+
(address coin1, ) = factory.deployTrendCoin("TEST123", address(0), "");
|
|
253
|
+
assertTrue(coin1 != address(0), "Coin with letters then numbers should deploy");
|
|
254
|
+
|
|
255
|
+
(address coin2, ) = factory.deployTrendCoin("123TEST", address(0), "");
|
|
256
|
+
assertTrue(coin2 != address(0), "Coin with numbers then letters should deploy");
|
|
257
|
+
|
|
258
|
+
(address coin3, ) = factory.deployTrendCoin("T1E2S3T", address(0), "");
|
|
259
|
+
assertTrue(coin3 != address(0), "Coin with alternating letters and numbers should deploy");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function test_deployTrendCoin_invalidSymbols_lettersAndDash() public {
|
|
263
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
264
|
+
factory.deployTrendCoin("TEST-COIN", address(0), "");
|
|
265
|
+
|
|
266
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
267
|
+
factory.deployTrendCoin("-TEST-", address(0), "");
|
|
268
|
+
|
|
269
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
270
|
+
factory.deployTrendCoin("TEST--COIN", address(0), "");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function test_deployTrendCoin_validSymbols_alphanumericMixed() public {
|
|
274
|
+
(address coin1, ) = factory.deployTrendCoin("TEST123Coin", address(0), "");
|
|
275
|
+
assertTrue(coin1 != address(0), "Coin with all alphanumeric types should deploy");
|
|
276
|
+
|
|
277
|
+
(address coin2, ) = factory.deployTrendCoin("ABC123XYZ", address(0), "");
|
|
278
|
+
assertTrue(coin2 != address(0), "Coin with mixed valid characters should deploy");
|
|
279
|
+
|
|
280
|
+
(address coin3, ) = factory.deployTrendCoin("Test456Coin789", address(0), "");
|
|
281
|
+
assertTrue(coin3 != address(0), "Coin with complex valid pattern should deploy");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function test_deployTrendCoin_validSymbols_lengthBoundaries() public {
|
|
285
|
+
// Minimum valid length (2 characters)
|
|
286
|
+
(address coin1, ) = factory.deployTrendCoin("AB", address(0), "");
|
|
287
|
+
assertTrue(coin1 != address(0), "Coin with 2 characters should deploy");
|
|
288
|
+
|
|
289
|
+
// Maximum valid length (32 characters)
|
|
290
|
+
(address coin2, ) = factory.deployTrendCoin("ABCDEFGHIJKLMNOPQRSTUVWXYZ123456", address(0), "");
|
|
291
|
+
assertTrue(coin2 != address(0), "Coin with 32 characters should deploy");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function test_deployTrendCoin_invalidSymbols_tooShort() public {
|
|
295
|
+
vm.expectRevert(ITrendCoinErrors.TickerTooShort.selector);
|
|
296
|
+
factory.deployTrendCoin("A", address(0), "");
|
|
297
|
+
|
|
298
|
+
vm.expectRevert(ITrendCoinErrors.TickerTooShort.selector);
|
|
299
|
+
factory.deployTrendCoin("", address(0), "");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function test_deployTrendCoin_invalidSymbols_tooLong() public {
|
|
303
|
+
vm.expectRevert(ITrendCoinErrors.TickerTooLong.selector);
|
|
304
|
+
factory.deployTrendCoin("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567", address(0), "");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ============ Fee Distribution Tests ============
|
|
308
|
+
|
|
309
|
+
function _deployTrendCoin() internal {
|
|
310
|
+
string memory symbol = "FEETEST";
|
|
311
|
+
(address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
|
|
312
|
+
trendCoin = TrendCoin(coinAddress);
|
|
313
|
+
vm.label(address(trendCoin), "TEST_TREND_COIN");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function _estimateLpFees(bytes memory commands, bytes[] memory inputs) internal returns (FeeEstimatorHook.FeeEstimatorState memory feeState) {
|
|
317
|
+
uint256 snapshot = vm.snapshotState();
|
|
318
|
+
_deployFeeEstimatorHook(address(hook));
|
|
319
|
+
|
|
320
|
+
// Execute the swap
|
|
321
|
+
uint256 deadline = block.timestamp + 20;
|
|
322
|
+
router.execute(commands, inputs, deadline);
|
|
323
|
+
|
|
324
|
+
feeState = FeeEstimatorHook(payable(address(hook))).getFeeState();
|
|
325
|
+
|
|
326
|
+
vm.revertToState(snapshot);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function _recordZoraBalances() internal view returns (RewardBalances memory balances) {
|
|
330
|
+
// For TrendCoins: creator and platformReferrer are address(0), so those balances will be 0
|
|
331
|
+
balances.creator = 0; // No creator for trend coins
|
|
332
|
+
balances.platformReferrer = 0; // No platform referrer for trend coins
|
|
333
|
+
balances.tradeReferrer = 0; // We'll track this if provided
|
|
334
|
+
balances.protocol = zoraToken.balanceOf(trendCoin.protocolRewardRecipient());
|
|
335
|
+
balances.doppler = zoraToken.balanceOf(trendCoin.dopplerFeeRecipient());
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function _buyTrendCoin(uint128 amountIn) internal returns (uint256 feeCurrency) {
|
|
339
|
+
deal(address(zoraToken), users.buyer, amountIn);
|
|
340
|
+
|
|
341
|
+
vm.warp(block.timestamp + 1 days);
|
|
342
|
+
|
|
343
|
+
vm.startPrank(users.buyer);
|
|
344
|
+
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(zoraToken), uint128(amountIn), uint48(block.timestamp + 1 days));
|
|
345
|
+
|
|
346
|
+
(bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
|
|
347
|
+
address(zoraToken),
|
|
348
|
+
uint128(amountIn),
|
|
349
|
+
address(trendCoin),
|
|
350
|
+
0,
|
|
351
|
+
trendCoin.getPoolKey(),
|
|
352
|
+
bytes("") // No trade referrer
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// Estimate the total fees before executing
|
|
356
|
+
FeeEstimatorHook.FeeEstimatorState memory feeState = _estimateLpFees(commands, inputs);
|
|
357
|
+
|
|
358
|
+
feeCurrency = feeState.afterSwapCurrencyAmount;
|
|
359
|
+
|
|
360
|
+
router.execute(commands, inputs, block.timestamp + 1 days);
|
|
361
|
+
vm.stopPrank();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/// @notice Test that TrendCoin fee distribution sends 100% to protocol with no LP remint
|
|
365
|
+
/// For TrendCoins, all market rewards go to protocolRewardRecipient and LP remint is skipped.
|
|
366
|
+
/// Note: In Foundry tests, transient storage persists for the whole test (single tx), so
|
|
367
|
+
/// isDeploying stays true and _calculateLaunchFee always returns LP_FEE_V4 (1%).
|
|
368
|
+
/// In production (separate txs), trend coins use TREND_LP_FEE_V4 (0.01%) after launch.
|
|
369
|
+
function test_trendCoin_feeDistribution_100PercentToProtocol() public {
|
|
370
|
+
_deployTrendCoin();
|
|
371
|
+
|
|
372
|
+
// Roll forward past the launch fee period (10 seconds) to avoid high launch fees
|
|
373
|
+
vm.warp(block.timestamp + 11 seconds);
|
|
374
|
+
|
|
375
|
+
uint128 tradeAmount = 1000 ether; // 1000 ZORA tokens
|
|
376
|
+
|
|
377
|
+
// Record initial protocol balance
|
|
378
|
+
address protocolRecipient = trendCoin.protocolRewardRecipient();
|
|
379
|
+
uint256 initialProtocolBalance = zoraToken.balanceOf(protocolRecipient);
|
|
380
|
+
|
|
381
|
+
// Perform trade (note: _buyTrendCoin also does vm.warp but this makes explicit we're past launch)
|
|
382
|
+
_buyTrendCoin(tradeAmount);
|
|
383
|
+
|
|
384
|
+
// Calculate final protocol balance
|
|
385
|
+
uint256 finalProtocolBalance = zoraToken.balanceOf(protocolRecipient);
|
|
386
|
+
uint256 protocolReward = finalProtocolBalance - initialProtocolBalance;
|
|
387
|
+
|
|
388
|
+
// Fee breakdown for TrendCoins in test (isDeploying=true due to transient storage):
|
|
389
|
+
// - Total fee: 1% (LP_FEE_V4, because isDeploying bypass is active)
|
|
390
|
+
// - No LP remint (skipped for trend coins)
|
|
391
|
+
// - Protocol gets 100% of fees
|
|
392
|
+
//
|
|
393
|
+
// Expected calculation:
|
|
394
|
+
// - Total fees = tradeAmount * 1% = 10 ZORA
|
|
395
|
+
// - Protocol receives = 10 ZORA (100% of fees, no LP remint for TrendCoins)
|
|
396
|
+
uint256 expectedTotalFees = (uint256(tradeAmount) * CoinConstants.LP_FEE_V4) / 1_000_000;
|
|
397
|
+
|
|
398
|
+
// Protocol should receive approximately all fees (allowing small rounding tolerance)
|
|
399
|
+
assertApproxEqRel(protocolReward, expectedTotalFees, 0.01e18, "Protocol should receive ~100% of total fees");
|
|
400
|
+
|
|
401
|
+
// Verify actual value is reasonable (should be ~10 ZORA for 1000 ZORA trade at 1% fee)
|
|
402
|
+
assertGt(protocolReward, 9.9 ether, "Protocol reward should be > 9.9 ZORA");
|
|
403
|
+
assertLt(protocolReward, 10.1 ether, "Protocol reward should be < 10.1 ZORA");
|
|
404
|
+
|
|
405
|
+
// Verify TrendCoin recipients are correctly configured (no creator/referrer rewards)
|
|
406
|
+
assertEq(trendCoin.payoutRecipient(), address(0), "Payout recipient should be zero");
|
|
407
|
+
assertEq(trendCoin.platformReferrer(), address(0), "Platform referrer should be zero");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/// @notice Test that TrendCoin has correct coin type
|
|
411
|
+
function test_trendCoin_coinType() public {
|
|
412
|
+
_deployTrendCoin();
|
|
413
|
+
|
|
414
|
+
IHasCoinType.CoinType theCoinType = CoinRewardsV4.getCoinType(IHasRewardsRecipients(address(trendCoin)));
|
|
415
|
+
assertEq(uint8(theCoinType), uint8(IHasCoinType.CoinType.Trend), "Should be Trend coin type");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/// @notice Test that TrendCoin recipients return expected values
|
|
419
|
+
function test_trendCoin_rewardRecipients() public {
|
|
420
|
+
_deployTrendCoin();
|
|
421
|
+
|
|
422
|
+
// Payout recipient and platform referrer should be address(0)
|
|
423
|
+
assertEq(trendCoin.payoutRecipient(), address(0), "Payout recipient should be zero");
|
|
424
|
+
assertEq(trendCoin.platformReferrer(), address(0), "Platform referrer should be zero");
|
|
425
|
+
|
|
426
|
+
// Protocol reward recipient should be set
|
|
427
|
+
assertTrue(trendCoin.protocolRewardRecipient() != address(0), "Protocol recipient should be set");
|
|
428
|
+
|
|
429
|
+
// Doppler fee recipient should be set
|
|
430
|
+
assertTrue(trendCoin.dopplerFeeRecipient() != address(0), "Doppler recipient should be set");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ============ Zero Fee After Launch Tests ============
|
|
434
|
+
|
|
435
|
+
/// @notice TrendCoins should have 0.01% swap fee after the launch fee duration
|
|
436
|
+
/// forge-config: default.isolate = true
|
|
437
|
+
function test_trendCoin_minimalFeeAfterLaunchDuration() public {
|
|
438
|
+
_deployTrendCoin();
|
|
439
|
+
|
|
440
|
+
uint128 amountIn = 100 ether;
|
|
441
|
+
address trader = makeAddr("trader");
|
|
442
|
+
|
|
443
|
+
// Snapshot at same pool state for both swaps
|
|
444
|
+
uint256 snapshot = vm.snapshotState();
|
|
445
|
+
|
|
446
|
+
// Swap right at the end of launch period (0.01% fee for trend coins)
|
|
447
|
+
vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION);
|
|
448
|
+
deal(address(zoraToken), trader, amountIn);
|
|
449
|
+
vm.startPrank(trader);
|
|
450
|
+
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(zoraToken), amountIn, uint48(block.timestamp + 1 days));
|
|
451
|
+
(bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
|
|
452
|
+
address(zoraToken),
|
|
453
|
+
amountIn,
|
|
454
|
+
address(trendCoin),
|
|
455
|
+
0,
|
|
456
|
+
trendCoin.getPoolKey(),
|
|
457
|
+
bytes("")
|
|
458
|
+
);
|
|
459
|
+
router.execute(commands, inputs, block.timestamp + 1 days);
|
|
460
|
+
vm.stopPrank();
|
|
461
|
+
uint256 coinsAtDuration = trendCoin.balanceOf(trader);
|
|
462
|
+
|
|
463
|
+
vm.revertToState(snapshot);
|
|
464
|
+
|
|
465
|
+
// Swap well after launch period — should yield same amount (both 0.01% fee)
|
|
466
|
+
vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION + 1 days);
|
|
467
|
+
deal(address(zoraToken), trader, amountIn);
|
|
468
|
+
vm.startPrank(trader);
|
|
469
|
+
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(zoraToken), amountIn, uint48(block.timestamp + 1 days));
|
|
470
|
+
(commands, inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
|
|
471
|
+
address(zoraToken),
|
|
472
|
+
amountIn,
|
|
473
|
+
address(trendCoin),
|
|
474
|
+
0,
|
|
475
|
+
trendCoin.getPoolKey(),
|
|
476
|
+
bytes("")
|
|
477
|
+
);
|
|
478
|
+
router.execute(commands, inputs, block.timestamp + 1 days);
|
|
479
|
+
vm.stopPrank();
|
|
480
|
+
uint256 coinsAfterDuration = trendCoin.balanceOf(trader);
|
|
481
|
+
|
|
482
|
+
// Both should be approximately equal (0.01% fee in both cases, same pool state)
|
|
483
|
+
assertApproxEqRel(coinsAtDuration, coinsAfterDuration, 0.01e18, "both swaps should yield same coins at 0.01% fee");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/// @notice TrendCoins should still have the launch fee during the launch period
|
|
487
|
+
/// forge-config: default.isolate = true
|
|
488
|
+
function test_trendCoin_launchFeeStillAppliesDuringLaunch() public {
|
|
489
|
+
_deployTrendCoin();
|
|
490
|
+
|
|
491
|
+
uint128 amountIn = 100 ether;
|
|
492
|
+
address trader = makeAddr("trader");
|
|
493
|
+
|
|
494
|
+
uint256 snapshot = vm.snapshotState();
|
|
495
|
+
|
|
496
|
+
// Swap immediately (launch fee ~99%)
|
|
497
|
+
deal(address(zoraToken), trader, amountIn);
|
|
498
|
+
vm.startPrank(trader);
|
|
499
|
+
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(zoraToken), amountIn, uint48(block.timestamp + 1 days));
|
|
500
|
+
(bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
|
|
501
|
+
address(zoraToken),
|
|
502
|
+
amountIn,
|
|
503
|
+
address(trendCoin),
|
|
504
|
+
0,
|
|
505
|
+
trendCoin.getPoolKey(),
|
|
506
|
+
bytes("")
|
|
507
|
+
);
|
|
508
|
+
router.execute(commands, inputs, block.timestamp + 1 days);
|
|
509
|
+
vm.stopPrank();
|
|
510
|
+
uint256 coinsAtLaunch = trendCoin.balanceOf(trader);
|
|
511
|
+
|
|
512
|
+
vm.revertToState(snapshot);
|
|
513
|
+
|
|
514
|
+
// Swap after launch period (0.01% fee for trend coins)
|
|
515
|
+
vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION + 1);
|
|
516
|
+
deal(address(zoraToken), trader, amountIn);
|
|
517
|
+
vm.startPrank(trader);
|
|
518
|
+
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(zoraToken), amountIn, uint48(block.timestamp + 1 days));
|
|
519
|
+
(commands, inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
|
|
520
|
+
address(zoraToken),
|
|
521
|
+
amountIn,
|
|
522
|
+
address(trendCoin),
|
|
523
|
+
0,
|
|
524
|
+
trendCoin.getPoolKey(),
|
|
525
|
+
bytes("")
|
|
526
|
+
);
|
|
527
|
+
router.execute(commands, inputs, block.timestamp + 1 days);
|
|
528
|
+
vm.stopPrank();
|
|
529
|
+
uint256 coinsPostLaunch = trendCoin.balanceOf(trader);
|
|
530
|
+
|
|
531
|
+
// Post-launch (0.01% fee) should yield significantly more coins than during launch (~99% fee)
|
|
532
|
+
assertGt(coinsPostLaunch, coinsAtLaunch, "should receive more coins after launch fee ends");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/// @notice Test that TrendCoin uses correct production curve configuration
|
|
536
|
+
function test_trendCoin_curveConfiguration() public {
|
|
537
|
+
// Deploy a trend coin
|
|
538
|
+
factory.deployTrendCoin("CURVETEST", address(0), "");
|
|
539
|
+
|
|
540
|
+
// Decode the production pool config
|
|
541
|
+
(
|
|
542
|
+
uint8 version,
|
|
543
|
+
address currency,
|
|
544
|
+
int24[] memory tickLower,
|
|
545
|
+
int24[] memory tickUpper,
|
|
546
|
+
uint16[] memory numDiscoveryPositions,
|
|
547
|
+
uint256[] memory maxDiscoverySupplyShare
|
|
548
|
+
) = CoinConfigurationVersions.decodeDopplerMultiCurveUniV4(CoinConstants.TREND_COIN_DEFAULT_POOL_CONFIG);
|
|
549
|
+
|
|
550
|
+
// Verify version is 4 (Doppler Multi-Curve Uni V4)
|
|
551
|
+
assertEq(version, 4, "Version should be 4");
|
|
552
|
+
|
|
553
|
+
// Verify currency is ZORA
|
|
554
|
+
assertEq(currency, CoinConstants.CREATOR_COIN_CURRENCY, "Currency should be ZORA");
|
|
555
|
+
|
|
556
|
+
// Verify 3 curves
|
|
557
|
+
assertEq(tickLower.length, 3, "Should have 3 curves");
|
|
558
|
+
assertEq(tickUpper.length, 3, "Should have 3 curves");
|
|
559
|
+
assertEq(numDiscoveryPositions.length, 3, "Should have 3 curves");
|
|
560
|
+
assertEq(maxDiscoverySupplyShare.length, 3, "Should have 3 curves");
|
|
561
|
+
|
|
562
|
+
// Verify Curve 1: ticks [-89200, -75200], 11 positions, 5% max supply
|
|
563
|
+
assertEq(tickLower[0], -89200, "Curve 1 lower tick should be -89200");
|
|
564
|
+
assertEq(tickUpper[0], -75200, "Curve 1 upper tick should be -75200");
|
|
565
|
+
assertEq(numDiscoveryPositions[0], 11, "Curve 1 should have 11 positions");
|
|
566
|
+
assertEq(maxDiscoverySupplyShare[0], 0.05e18, "Curve 1 max supply should be 5%");
|
|
567
|
+
|
|
568
|
+
// Verify Curve 2: ticks [-77200, -68200], 11 positions, 12.5% max supply
|
|
569
|
+
assertEq(tickLower[1], -77200, "Curve 2 lower tick should be -77200");
|
|
570
|
+
assertEq(tickUpper[1], -68200, "Curve 2 upper tick should be -68200");
|
|
571
|
+
assertEq(numDiscoveryPositions[1], 11, "Curve 2 should have 11 positions");
|
|
572
|
+
assertEq(maxDiscoverySupplyShare[1], 0.125e18, "Curve 2 max supply should be 12.5%");
|
|
573
|
+
|
|
574
|
+
// Verify Curve 3: ticks [-71200, -68200], 11 positions, 20% max supply
|
|
575
|
+
assertEq(tickLower[2], -71200, "Curve 3 lower tick should be -71200");
|
|
576
|
+
assertEq(tickUpper[2], -68200, "Curve 3 upper tick should be -68200");
|
|
577
|
+
assertEq(numDiscoveryPositions[2], 11, "Curve 3 should have 11 positions");
|
|
578
|
+
assertEq(maxDiscoverySupplyShare[2], 0.20e18, "Curve 3 max supply should be 20%");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ============ Post-Deploy Hook Tests ============
|
|
582
|
+
|
|
583
|
+
function test_deployTrendCoin_withNullHook() public {
|
|
584
|
+
string memory symbol = "NULLHOOK";
|
|
585
|
+
|
|
586
|
+
// Deploy with null hook (backward compatible behavior)
|
|
587
|
+
(address coinAddress, bytes memory hookDataOut) = factory.deployTrendCoin(symbol, address(0), "");
|
|
588
|
+
|
|
589
|
+
// Verify coin was created
|
|
590
|
+
assertTrue(coinAddress != address(0), "Coin should be created");
|
|
591
|
+
assertEq(hookDataOut.length, 0, "Hook data should be empty");
|
|
592
|
+
|
|
593
|
+
// Verify TrendCoin properties
|
|
594
|
+
TrendCoin coin = TrendCoin(payable(coinAddress));
|
|
595
|
+
assertEq(coin.symbol(), symbol, "Symbol should match");
|
|
596
|
+
assertEq(uint8(coin.coinType()), uint8(IHasCoinType.CoinType.Trend), "Should be Trend coin type");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function test_revertWhen_deployTrendCoin_ethWithoutHook() public {
|
|
600
|
+
string memory symbol = "ETHERROR";
|
|
601
|
+
|
|
602
|
+
// Should revert when sending ETH without hook
|
|
603
|
+
vm.expectRevert(IZoraFactory.EthTransferInvalid.selector);
|
|
604
|
+
factory.deployTrendCoin{value: 1 ether}(symbol, address(0), "");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function test_revertWhen_deployTrendCoin_invalidHook() public {
|
|
608
|
+
string memory symbol = "BADHOOK";
|
|
609
|
+
|
|
610
|
+
// Create invalid hook (EOA without code doesn't implement IHasAfterCoinDeploy)
|
|
611
|
+
address invalidHook = makeAddr("invalidHook");
|
|
612
|
+
|
|
613
|
+
vm.expectRevert();
|
|
614
|
+
factory.deployTrendCoin{value: 1 ether}(symbol, invalidHook, "");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function test_revertWhen_deployTrendCoin_tickerAlreadyUsedWithHook() public {
|
|
618
|
+
string memory symbol = "DUPLICATE";
|
|
619
|
+
|
|
620
|
+
// Deploy first coin without hook
|
|
621
|
+
factory.deployTrendCoin(symbol, address(0), "");
|
|
622
|
+
|
|
623
|
+
// Try to deploy with same ticker - should revert even with hook address
|
|
624
|
+
vm.expectRevert(abi.encodeWithSelector(ITrendCoinErrors.TickerAlreadyUsed.selector, symbol));
|
|
625
|
+
factory.deployTrendCoin(symbol, makeAddr("someHook"), "");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function test_deployTrendCoin_addressPredictionWithHooks() public {
|
|
629
|
+
string memory symbol = "PREDICT2";
|
|
630
|
+
|
|
631
|
+
// Get predicted address (should work regardless of hook params)
|
|
632
|
+
address predictedAddress = factory.trendCoinAddress(symbol);
|
|
633
|
+
|
|
634
|
+
// Deploy with non-null hook address (but still null since no valid hook implementation)
|
|
635
|
+
(address actualAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
|
|
636
|
+
|
|
637
|
+
// Prediction should still match (address based on ticker only)
|
|
638
|
+
assertEq(actualAddress, predictedAddress, "Predicted address should match");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function test_deployTrendCoin_tickerUniquenessWithHooks() public {
|
|
642
|
+
// Deploy with null hook
|
|
643
|
+
factory.deployTrendCoin("test", address(0), "");
|
|
644
|
+
|
|
645
|
+
// Try to deploy with same ticker, different case - should revert
|
|
646
|
+
vm.expectRevert(abi.encodeWithSelector(ITrendCoinErrors.TickerAlreadyUsed.selector, "TEST"));
|
|
647
|
+
factory.deployTrendCoin("TEST", address(0), "");
|
|
648
|
+
|
|
649
|
+
// Try with mixed case - should also revert
|
|
650
|
+
vm.expectRevert(abi.encodeWithSelector(ITrendCoinErrors.TickerAlreadyUsed.selector, "TeSt"));
|
|
651
|
+
factory.deployTrendCoin("TeSt", address(0), "");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ============ URI Tests ============
|
|
655
|
+
|
|
656
|
+
function test_deployTrendCoin_uri_usesSymbolDirectly() public {
|
|
657
|
+
string memory symbol = "TESTCOIN";
|
|
658
|
+
(address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
|
|
659
|
+
|
|
660
|
+
trendCoin = TrendCoin(coinAddress);
|
|
661
|
+
string memory uri = trendCoin.tokenURI();
|
|
662
|
+
|
|
663
|
+
assertEq(uri, "https://trends.theme.wtf/trend/TESTCOIN", "URI should use symbol directly");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function test_deployTrendCoin_uri_preservesCase() public {
|
|
667
|
+
string memory symbol = "TestCoin";
|
|
668
|
+
(address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
|
|
669
|
+
|
|
670
|
+
trendCoin = TrendCoin(coinAddress);
|
|
671
|
+
string memory uri = trendCoin.tokenURI();
|
|
672
|
+
|
|
673
|
+
assertEq(uri, "https://trends.theme.wtf/trend/TestCoin", "URI should preserve original case");
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function test_deployTrendCoin_uri_symbolAndNameMatch() public {
|
|
677
|
+
string memory symbol = "TESTCOIN";
|
|
678
|
+
(address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
|
|
679
|
+
|
|
680
|
+
trendCoin = TrendCoin(coinAddress);
|
|
681
|
+
|
|
682
|
+
assertEq(trendCoin.symbol(), symbol, "Symbol should match");
|
|
683
|
+
assertEq(trendCoin.name(), symbol, "Name should equal symbol for trend coins");
|
|
684
|
+
assertEq(trendCoin.tokenURI(), string.concat("https://trends.theme.wtf/trend/", symbol), "URI should be base URI + symbol");
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ============ Metadata Manager Tests ============
|
|
688
|
+
|
|
689
|
+
function test_setContractURI_byMetadataManager() public {
|
|
690
|
+
_deployTrendCoin();
|
|
691
|
+
|
|
692
|
+
string memory newURI = "https://example.com/updated-metadata";
|
|
693
|
+
|
|
694
|
+
// Metadata manager should be able to update the contract URI
|
|
695
|
+
vm.prank(users.metadataManager);
|
|
696
|
+
trendCoin.setContractURI(newURI);
|
|
697
|
+
|
|
698
|
+
assertEq(trendCoin.contractURI(), newURI, "Contract URI should be updated");
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function test_setContractURI_multipleUpdates() public {
|
|
702
|
+
_deployTrendCoin();
|
|
703
|
+
|
|
704
|
+
string memory uri1 = "https://example.com/v1";
|
|
705
|
+
string memory uri2 = "https://example.com/v2";
|
|
706
|
+
string memory uri3 = "https://example.com/v3";
|
|
707
|
+
|
|
708
|
+
vm.startPrank(users.metadataManager);
|
|
709
|
+
|
|
710
|
+
trendCoin.setContractURI(uri1);
|
|
711
|
+
assertEq(trendCoin.contractURI(), uri1, "Contract URI should be v1");
|
|
712
|
+
|
|
713
|
+
trendCoin.setContractURI(uri2);
|
|
714
|
+
assertEq(trendCoin.contractURI(), uri2, "Contract URI should be v2");
|
|
715
|
+
|
|
716
|
+
trendCoin.setContractURI(uri3);
|
|
717
|
+
assertEq(trendCoin.contractURI(), uri3, "Contract URI should be v3");
|
|
718
|
+
|
|
719
|
+
vm.stopPrank();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function test_revertWhen_setContractURI_byNonMetadataManager() public {
|
|
723
|
+
_deployTrendCoin();
|
|
724
|
+
|
|
725
|
+
string memory newURI = "https://example.com/malicious";
|
|
726
|
+
|
|
727
|
+
// Random address should not be able to update the contract URI
|
|
728
|
+
address randomUser = makeAddr("randomUser");
|
|
729
|
+
vm.prank(randomUser);
|
|
730
|
+
vm.expectRevert(ITrendCoin.OnlyMetadataManager.selector);
|
|
731
|
+
trendCoin.setContractURI(newURI);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function test_revertWhen_setContractURI_byOwner() public {
|
|
735
|
+
_deployTrendCoin();
|
|
736
|
+
|
|
737
|
+
string memory newURI = "https://example.com/owner-attempt";
|
|
738
|
+
|
|
739
|
+
// Even the coin owner should not be able to use setContractURI (must use setContractURI)
|
|
740
|
+
vm.prank(users.creator);
|
|
741
|
+
vm.expectRevert(ITrendCoin.OnlyMetadataManager.selector);
|
|
742
|
+
trendCoin.setContractURI(newURI);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function test_revertWhen_setContractURI_byFactoryOwner() public {
|
|
746
|
+
_deployTrendCoin();
|
|
747
|
+
|
|
748
|
+
string memory newURI = "https://example.com/factory-owner-attempt";
|
|
749
|
+
|
|
750
|
+
// Factory owner should not be able to update the contract URI
|
|
751
|
+
vm.prank(users.factoryOwner);
|
|
752
|
+
vm.expectRevert(ITrendCoin.OnlyMetadataManager.selector);
|
|
753
|
+
trendCoin.setContractURI(newURI);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function test_setContractURI_emptyString() public {
|
|
757
|
+
_deployTrendCoin();
|
|
758
|
+
|
|
759
|
+
// Metadata manager should be able to set empty URI
|
|
760
|
+
vm.prank(users.metadataManager);
|
|
761
|
+
trendCoin.setContractURI("");
|
|
762
|
+
|
|
763
|
+
assertEq(trendCoin.contractURI(), "", "Contract URI should be empty");
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ============ setNameAndSymbol Metadata Manager Tests ============
|
|
767
|
+
|
|
768
|
+
function test_setNameAndSymbol_byMetadataManager() public {
|
|
769
|
+
_deployTrendCoin();
|
|
770
|
+
|
|
771
|
+
string memory newName = "UpdatedTrend";
|
|
772
|
+
string memory newSymbol = "UPDTREND";
|
|
773
|
+
|
|
774
|
+
vm.prank(users.metadataManager);
|
|
775
|
+
trendCoin.setNameAndSymbol(newName, newSymbol);
|
|
776
|
+
|
|
777
|
+
assertEq(trendCoin.name(), newName, "Name should be updated");
|
|
778
|
+
assertEq(trendCoin.symbol(), newSymbol, "Symbol should be updated");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function test_setNameAndSymbol_multipleUpdates() public {
|
|
782
|
+
_deployTrendCoin();
|
|
783
|
+
|
|
784
|
+
vm.startPrank(users.metadataManager);
|
|
785
|
+
|
|
786
|
+
trendCoin.setNameAndSymbol("Name1", "SYM1");
|
|
787
|
+
assertEq(trendCoin.name(), "Name1", "Name should be Name1");
|
|
788
|
+
assertEq(trendCoin.symbol(), "SYM1", "Symbol should be SYM1");
|
|
789
|
+
|
|
790
|
+
trendCoin.setNameAndSymbol("Name2", "SYM2");
|
|
791
|
+
assertEq(trendCoin.name(), "Name2", "Name should be Name2");
|
|
792
|
+
assertEq(trendCoin.symbol(), "SYM2", "Symbol should be SYM2");
|
|
793
|
+
|
|
794
|
+
vm.stopPrank();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function test_setNameAndSymbol_emitsEvent() public {
|
|
798
|
+
_deployTrendCoin();
|
|
799
|
+
|
|
800
|
+
string memory newName = "EventTrend";
|
|
801
|
+
string memory newSymbol = "EVTTREND";
|
|
802
|
+
|
|
803
|
+
vm.prank(users.metadataManager);
|
|
804
|
+
vm.expectEmit(true, true, true, true);
|
|
805
|
+
emit ICoin.NameAndSymbolUpdated(users.metadataManager, newName, newSymbol);
|
|
806
|
+
trendCoin.setNameAndSymbol(newName, newSymbol);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function test_revertWhen_setNameAndSymbol_byNonMetadataManager() public {
|
|
810
|
+
_deployTrendCoin();
|
|
811
|
+
|
|
812
|
+
address randomUser = makeAddr("randomUser");
|
|
813
|
+
vm.prank(randomUser);
|
|
814
|
+
vm.expectRevert(ITrendCoin.OnlyMetadataManager.selector);
|
|
815
|
+
trendCoin.setNameAndSymbol("Malicious", "MAL");
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function test_revertWhen_setNameAndSymbol_byOwner() public {
|
|
819
|
+
_deployTrendCoin();
|
|
820
|
+
|
|
821
|
+
vm.prank(users.creator);
|
|
822
|
+
vm.expectRevert(ITrendCoin.OnlyMetadataManager.selector);
|
|
823
|
+
trendCoin.setNameAndSymbol("OwnerAttempt", "OWN");
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function test_revertWhen_setNameAndSymbol_byFactoryOwner() public {
|
|
827
|
+
_deployTrendCoin();
|
|
828
|
+
|
|
829
|
+
vm.prank(users.factoryOwner);
|
|
830
|
+
vm.expectRevert(ITrendCoin.OnlyMetadataManager.selector);
|
|
831
|
+
trendCoin.setNameAndSymbol("FactoryAttempt", "FAC");
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function test_revertWhen_setNameAndSymbol_emptyName() public {
|
|
835
|
+
_deployTrendCoin();
|
|
836
|
+
|
|
837
|
+
vm.prank(users.metadataManager);
|
|
838
|
+
vm.expectRevert(abi.encodeWithSelector(ICoin.NameIsRequired.selector));
|
|
839
|
+
trendCoin.setNameAndSymbol("", "SYM");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ============ Pool Config Admin Tests ============
|
|
843
|
+
|
|
844
|
+
function _getDefaultPoolConfigParams()
|
|
845
|
+
internal
|
|
846
|
+
pure
|
|
847
|
+
returns (
|
|
848
|
+
address currency,
|
|
849
|
+
int24[] memory tickLower,
|
|
850
|
+
int24[] memory tickUpper,
|
|
851
|
+
uint16[] memory numDiscoveryPositions,
|
|
852
|
+
uint256[] memory maxDiscoverySupplyShare
|
|
853
|
+
)
|
|
854
|
+
{
|
|
855
|
+
(, currency, tickLower, tickUpper, numDiscoveryPositions, maxDiscoverySupplyShare) = CoinConfigurationVersions.decodeDopplerMultiCurveUniV4(
|
|
856
|
+
CoinConstants.TREND_COIN_DEFAULT_POOL_CONFIG
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function test_setTrendCoinPoolConfig_ownerCanSet() public {
|
|
861
|
+
bytes memory expectedPoolConfig = CoinConstants.TREND_COIN_DEFAULT_POOL_CONFIG;
|
|
862
|
+
|
|
863
|
+
// Get the factory owner
|
|
864
|
+
address factoryOwner = ZoraFactoryImpl(address(factory)).owner();
|
|
865
|
+
|
|
866
|
+
(
|
|
867
|
+
address currency,
|
|
868
|
+
int24[] memory tickLower,
|
|
869
|
+
int24[] memory tickUpper,
|
|
870
|
+
uint16[] memory numDiscoveryPositions,
|
|
871
|
+
uint256[] memory maxDiscoverySupplyShare
|
|
872
|
+
) = _getDefaultPoolConfigParams();
|
|
873
|
+
|
|
874
|
+
vm.prank(factoryOwner);
|
|
875
|
+
factory.setTrendCoinPoolConfig(currency, tickLower, tickUpper, numDiscoveryPositions, maxDiscoverySupplyShare);
|
|
876
|
+
|
|
877
|
+
// Verify it was set
|
|
878
|
+
bytes memory storedConfig = factory.trendCoinPoolConfig();
|
|
879
|
+
assertEq(keccak256(storedConfig), keccak256(expectedPoolConfig), "Pool config should be stored");
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function test_setTrendCoinPoolConfig_emitsEvent() public {
|
|
883
|
+
bytes memory expectedPoolConfig = CoinConstants.TREND_COIN_DEFAULT_POOL_CONFIG;
|
|
884
|
+
address factoryOwner = ZoraFactoryImpl(address(factory)).owner();
|
|
885
|
+
|
|
886
|
+
(
|
|
887
|
+
address currency,
|
|
888
|
+
int24[] memory tickLower,
|
|
889
|
+
int24[] memory tickUpper,
|
|
890
|
+
uint16[] memory numDiscoveryPositions,
|
|
891
|
+
uint256[] memory maxDiscoverySupplyShare
|
|
892
|
+
) = _getDefaultPoolConfigParams();
|
|
893
|
+
|
|
894
|
+
vm.expectEmit(false, false, false, true);
|
|
895
|
+
emit IZoraFactory.TrendCoinPoolConfigUpdated(expectedPoolConfig);
|
|
896
|
+
|
|
897
|
+
vm.prank(factoryOwner);
|
|
898
|
+
factory.setTrendCoinPoolConfig(currency, tickLower, tickUpper, numDiscoveryPositions, maxDiscoverySupplyShare);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function test_revertWhen_setTrendCoinPoolConfig_nonOwner() public {
|
|
902
|
+
address nonOwner = makeAddr("nonOwner");
|
|
903
|
+
|
|
904
|
+
(
|
|
905
|
+
address currency,
|
|
906
|
+
int24[] memory tickLower,
|
|
907
|
+
int24[] memory tickUpper,
|
|
908
|
+
uint16[] memory numDiscoveryPositions,
|
|
909
|
+
uint256[] memory maxDiscoverySupplyShare
|
|
910
|
+
) = _getDefaultPoolConfigParams();
|
|
911
|
+
|
|
912
|
+
vm.prank(nonOwner);
|
|
913
|
+
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, nonOwner));
|
|
914
|
+
factory.setTrendCoinPoolConfig(currency, tickLower, tickUpper, numDiscoveryPositions, maxDiscoverySupplyShare);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function test_revertWhen_setTrendCoinPoolConfig_emptyArrays() public {
|
|
918
|
+
address factoryOwner = ZoraFactoryImpl(address(factory)).owner();
|
|
919
|
+
address currency = CoinConstants.CREATOR_COIN_CURRENCY;
|
|
920
|
+
|
|
921
|
+
int24[] memory tickLower = new int24[](0);
|
|
922
|
+
int24[] memory tickUpper = new int24[](0);
|
|
923
|
+
uint16[] memory numDiscoveryPositions = new uint16[](0);
|
|
924
|
+
uint256[] memory maxDiscoverySupplyShare = new uint256[](0);
|
|
925
|
+
|
|
926
|
+
vm.prank(factoryOwner);
|
|
927
|
+
vm.expectRevert(IZoraFactory.InvalidConfig.selector);
|
|
928
|
+
factory.setTrendCoinPoolConfig(currency, tickLower, tickUpper, numDiscoveryPositions, maxDiscoverySupplyShare);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function test_revertWhen_setTrendCoinPoolConfig_mismatchedArrayLengths() public {
|
|
932
|
+
address factoryOwner = ZoraFactoryImpl(address(factory)).owner();
|
|
933
|
+
address currency = CoinConstants.CREATOR_COIN_CURRENCY;
|
|
934
|
+
|
|
935
|
+
int24[] memory tickLower = new int24[](2);
|
|
936
|
+
int24[] memory tickUpper = new int24[](3); // Mismatched length
|
|
937
|
+
uint16[] memory numDiscoveryPositions = new uint16[](2);
|
|
938
|
+
uint256[] memory maxDiscoverySupplyShare = new uint256[](2);
|
|
939
|
+
|
|
940
|
+
vm.prank(factoryOwner);
|
|
941
|
+
vm.expectRevert(IZoraFactory.InvalidConfig.selector);
|
|
942
|
+
factory.setTrendCoinPoolConfig(currency, tickLower, tickUpper, numDiscoveryPositions, maxDiscoverySupplyShare);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function test_revertWhen_deployTrendCoin_configNotSet() public {
|
|
946
|
+
// Deploy a fresh factory without pool config set
|
|
947
|
+
// We need to test this with a fresh factory that hasn't had the config set
|
|
948
|
+
// Since our test setup sets the config, we need to use a different approach
|
|
949
|
+
|
|
950
|
+
// Deploy a new factory proxy
|
|
951
|
+
address proxyShim = address(new ProxyShim());
|
|
952
|
+
ZoraFactory newFactoryProxy = new ZoraFactory(proxyShim);
|
|
953
|
+
|
|
954
|
+
// Create a new hook registry that includes the new factory as an owner
|
|
955
|
+
ZoraHookRegistry newHookRegistry = new ZoraHookRegistry();
|
|
956
|
+
address[] memory initialOwners = new address[](2);
|
|
957
|
+
initialOwners[0] = address(this);
|
|
958
|
+
initialOwners[1] = address(newFactoryProxy);
|
|
959
|
+
newHookRegistry.initialize(initialOwners);
|
|
960
|
+
|
|
961
|
+
// Create a new factory impl with the new hook registry
|
|
962
|
+
ZoraFactoryImpl newFactoryImpl = new ZoraFactoryImpl(
|
|
963
|
+
address(coinV4Impl),
|
|
964
|
+
address(creatorCoinImpl),
|
|
965
|
+
address(trendCoinImpl),
|
|
966
|
+
address(hook),
|
|
967
|
+
address(newHookRegistry)
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
// Upgrade to real impl and initialize
|
|
971
|
+
UUPSUpgradeable(address(newFactoryProxy)).upgradeToAndCall(
|
|
972
|
+
address(newFactoryImpl),
|
|
973
|
+
abi.encodeWithSelector(ZoraFactoryImpl.initialize.selector, address(this))
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
IZoraFactory newFactory = IZoraFactory(address(newFactoryProxy));
|
|
977
|
+
|
|
978
|
+
// Try to deploy trend coin without setting config first
|
|
979
|
+
vm.expectRevert(IZoraFactory.TrendCoinPoolConfigNotSet.selector);
|
|
980
|
+
newFactory.deployTrendCoin("NOCONFIG", address(0), "");
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function test_deployTrendCoin_usesStoredConfig() public {
|
|
984
|
+
// The factory should already have config set from setUp
|
|
985
|
+
// Deploy a trend coin and verify it works
|
|
986
|
+
string memory symbol = "CONFIGTEST";
|
|
987
|
+
|
|
988
|
+
(address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
|
|
989
|
+
|
|
990
|
+
// Verify the coin was created successfully
|
|
991
|
+
trendCoin = TrendCoin(coinAddress);
|
|
992
|
+
assertEq(trendCoin.symbol(), symbol, "Symbol should match");
|
|
993
|
+
assertEq(uint8(trendCoin.coinType()), uint8(IHasCoinType.CoinType.Trend), "Should be Trend coin type");
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function test_trendCoinPoolConfig_returnsStoredConfig() public {
|
|
997
|
+
bytes memory expectedConfig = CoinConstants.TREND_COIN_DEFAULT_POOL_CONFIG;
|
|
998
|
+
|
|
999
|
+
// Get stored config (should be set from setUp)
|
|
1000
|
+
bytes memory storedConfig = factory.trendCoinPoolConfig();
|
|
1001
|
+
|
|
1002
|
+
assertEq(keccak256(storedConfig), keccak256(expectedConfig), "Should return the stored config");
|
|
1003
|
+
}
|
|
1004
|
+
// ============ Reinitialization Protection Tests ============
|
|
1005
|
+
|
|
1006
|
+
function test_revertWhen_reinitializeTrendCoin() public {
|
|
1007
|
+
_deployTrendCoin();
|
|
1008
|
+
|
|
1009
|
+
// Get pool key and configuration from the deployed coin
|
|
1010
|
+
ICoin coin = ICoin(address(trendCoin));
|
|
1011
|
+
PoolKey memory poolKey_ = coin.getPoolKey();
|
|
1012
|
+
PoolConfiguration memory poolConfig_ = coin.getPoolConfiguration();
|
|
1013
|
+
|
|
1014
|
+
// Try to reinitialize via initializeTrendCoin - should fail
|
|
1015
|
+
address[] memory owners = new address[](1);
|
|
1016
|
+
owners[0] = users.creator;
|
|
1017
|
+
|
|
1018
|
+
vm.expectRevert(Initializable.InvalidInitialization.selector);
|
|
1019
|
+
trendCoin.initializeTrendCoin(owners, "REINIT", poolKey_, uint160(1 << 96), poolConfig_);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function test_revertWhen_legacyInitialize() public {
|
|
1023
|
+
_deployTrendCoin();
|
|
1024
|
+
|
|
1025
|
+
// Get pool key and configuration from the deployed coin
|
|
1026
|
+
ICoin coin = ICoin(address(trendCoin));
|
|
1027
|
+
PoolKey memory poolKey_ = coin.getPoolKey();
|
|
1028
|
+
PoolConfiguration memory poolConfig_ = coin.getPoolConfiguration();
|
|
1029
|
+
|
|
1030
|
+
// Try to call legacy initialize - should always revert with UseSpecificTrendCoinInitialize
|
|
1031
|
+
address[] memory owners = new address[](1);
|
|
1032
|
+
owners[0] = users.creator;
|
|
1033
|
+
|
|
1034
|
+
vm.expectRevert(ITrendCoinErrors.UseSpecificTrendCoinInitialize.selector);
|
|
1035
|
+
trendCoin.initialize(
|
|
1036
|
+
address(0),
|
|
1037
|
+
owners,
|
|
1038
|
+
"https://example.com/reinit",
|
|
1039
|
+
"REINIT",
|
|
1040
|
+
"REINIT",
|
|
1041
|
+
address(0),
|
|
1042
|
+
CoinConstants.CREATOR_COIN_CURRENCY,
|
|
1043
|
+
poolKey_,
|
|
1044
|
+
uint160(1 << 96),
|
|
1045
|
+
poolConfig_
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/// @notice Tests that exercise the real post-launch 0.01% fee path for trend coins.
|
|
1051
|
+
/// By deploying the coin in setUp(), the deployment transaction completes and transient
|
|
1052
|
+
/// storage (isDeploying) is cleared before any test function runs. This lets
|
|
1053
|
+
/// _calculateLaunchFee reach the TREND_LP_FEE_V4 branch instead of the isDeploying bypass.
|
|
1054
|
+
contract TrendCoinFeeTest is BaseTest {
|
|
1055
|
+
TrendCoin internal trendCoin;
|
|
1056
|
+
|
|
1057
|
+
function setUp() public override {
|
|
1058
|
+
super.setUpNonForked();
|
|
1059
|
+
|
|
1060
|
+
// Deploy in setUp so transient storage is cleared before tests run
|
|
1061
|
+
(address coinAddress, ) = factory.deployTrendCoin("FEETEST", address(0), "");
|
|
1062
|
+
trendCoin = TrendCoin(coinAddress);
|
|
1063
|
+
vm.label(address(trendCoin), "TEST_TREND_COIN");
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/// @notice Verify that trend coins charge 0.01% after the launch fee window
|
|
1067
|
+
function test_trendCoin_postLaunchFeeIs1Bps() public {
|
|
1068
|
+
// Warp past the launch fee period
|
|
1069
|
+
vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION + 1);
|
|
1070
|
+
|
|
1071
|
+
uint128 tradeAmount = 1_000_000 ether;
|
|
1072
|
+
|
|
1073
|
+
address protocolRecipient = trendCoin.protocolRewardRecipient();
|
|
1074
|
+
uint256 protocolBefore = zoraToken.balanceOf(protocolRecipient);
|
|
1075
|
+
|
|
1076
|
+
deal(address(zoraToken), users.buyer, tradeAmount);
|
|
1077
|
+
vm.startPrank(users.buyer);
|
|
1078
|
+
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(zoraToken), tradeAmount, uint48(block.timestamp + 1 days));
|
|
1079
|
+
(bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
|
|
1080
|
+
address(zoraToken),
|
|
1081
|
+
tradeAmount,
|
|
1082
|
+
address(trendCoin),
|
|
1083
|
+
0,
|
|
1084
|
+
trendCoin.getPoolKey(),
|
|
1085
|
+
bytes("")
|
|
1086
|
+
);
|
|
1087
|
+
router.execute(commands, inputs, block.timestamp + 1 days);
|
|
1088
|
+
vm.stopPrank();
|
|
1089
|
+
|
|
1090
|
+
uint256 protocolAfter = zoraToken.balanceOf(protocolRecipient);
|
|
1091
|
+
uint256 protocolReward = protocolAfter - protocolBefore;
|
|
1092
|
+
|
|
1093
|
+
// 0.01% of 1M = 100 ZORA. Allow 10% tolerance for swap path conversion.
|
|
1094
|
+
uint256 expectedFee = (uint256(tradeAmount) * CoinConstants.TREND_LP_FEE_V4) / 1_000_000;
|
|
1095
|
+
assertApproxEqRel(protocolReward, expectedFee, 0.10e18, "Protocol reward should be ~0.01% of trade");
|
|
1096
|
+
|
|
1097
|
+
// Sanity: must be far below what 1% would yield (10,000 ZORA)
|
|
1098
|
+
uint256 onePercentFee = (uint256(tradeAmount) * CoinConstants.LP_FEE_V4) / 1_000_000;
|
|
1099
|
+
assertLt(protocolReward, onePercentFee / 5, "Fee must be well below the 1% deployment-bypass level");
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/// @notice Smoke test that a post-launch trend coin swap still succeeds on the 1 bp fee path
|
|
1103
|
+
function test_trendCoin_postLaunchSwapSucceeds() public {
|
|
1104
|
+
vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION + 1);
|
|
1105
|
+
|
|
1106
|
+
uint128 amountIn = 100 ether;
|
|
1107
|
+
address trader = makeAddr("trader");
|
|
1108
|
+
|
|
1109
|
+
// Execute a trend coin swap
|
|
1110
|
+
deal(address(zoraToken), trader, amountIn);
|
|
1111
|
+
vm.startPrank(trader);
|
|
1112
|
+
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(zoraToken), amountIn, uint48(block.timestamp + 1 days));
|
|
1113
|
+
(bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
|
|
1114
|
+
address(zoraToken),
|
|
1115
|
+
amountIn,
|
|
1116
|
+
address(trendCoin),
|
|
1117
|
+
0,
|
|
1118
|
+
trendCoin.getPoolKey(),
|
|
1119
|
+
bytes("")
|
|
1120
|
+
);
|
|
1121
|
+
router.execute(commands, inputs, block.timestamp + 1 days);
|
|
1122
|
+
vm.stopPrank();
|
|
1123
|
+
|
|
1124
|
+
uint256 trendCoinsReceived = trendCoin.balanceOf(trader);
|
|
1125
|
+
|
|
1126
|
+
assertGt(trendCoinsReceived, 0, "Post-launch trend swap should return coins");
|
|
1127
|
+
}
|
|
1128
|
+
}
|