@zoralabs/coins 2.4.1 → 2.6.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.
- package/.abi-stability +923 -0
- package/.turbo/turbo-build$colon$js.log +143 -129
- package/CHANGELOG.md +38 -16
- package/abis/BaseCoin.json +23 -0
- package/abis/ContentCoin.json +23 -0
- package/abis/CreatorCoin.json +18 -0
- package/abis/ICoin.json +5 -0
- package/abis/ICoinV3.json +5 -0
- package/abis/IHasCreationInfo.json +20 -0
- package/abis/ITrendCoin.json +130 -0
- package/abis/ITrendCoinErrors.json +23 -0
- package/abis/IUniversalRouter.json +61 -0
- package/abis/IZoraFactory.json +227 -0
- package/abis/TrendCoin.json +2043 -0
- package/abis/ZoraFactoryImpl.json +232 -0
- package/dist/index.cjs +962 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +960 -117
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +1404 -131
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/package/wagmiGenerated.ts +970 -119
- package/package.json +4 -2
- package/src/BaseCoin.sol +44 -14
- 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 +73 -8
- package/src/interfaces/ICoin.sol +5 -1
- package/src/interfaces/ICreatorCoin.sol +0 -3
- package/src/interfaces/IHasCreationInfo.sol +12 -0
- package/src/interfaces/IPoolManager.sol +13 -0
- package/src/interfaces/ITrendCoin.sol +26 -0
- package/src/interfaces/ITrendCoinErrors.sol +18 -0
- package/src/interfaces/IZoraFactory.sol +60 -1
- package/src/libs/CoinConstants.sol +25 -1
- package/src/libs/CoinRewardsV4.sol +67 -19
- package/src/libs/CoinSetup.sol +7 -1
- package/src/libs/TickerUtils.sol +84 -0
- package/src/libs/UniV4SwapToCurrency.sol +2 -1
- package/src/libs/V3ToV4SwapLib.sol +7 -3
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/CoinUniV4.t.sol +4 -0
- package/test/ContentCoinRewards.t.sol +1 -0
- package/test/CreatorCoin.t.sol +2 -1
- package/test/CreatorCoinRewards.t.sol +1 -0
- package/test/Factory.t.sol +31 -5
- package/test/LaunchFee.t.sol +284 -0
- package/test/LiquidityMigration.t.sol +0 -2
- package/test/TrendCoin.t.sol +1077 -0
- package/test/Upgrades.t.sol +16 -3
- package/test/utils/FeeEstimatorHook.sol +33 -8
- package/test/utils/V4TestSetup.sol +36 -4
- package/wagmi.config.ts +2 -0
|
@@ -0,0 +1,284 @@
|
|
|
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 {CoinConstants} from "../src/libs/CoinConstants.sol";
|
|
8
|
+
import {IHasCreationInfo} from "../src/interfaces/IHasCreationInfo.sol";
|
|
9
|
+
import {UniV4SwapHelper} from "../src/libs/UniV4SwapHelper.sol";
|
|
10
|
+
import {ContentCoin} from "../src/ContentCoin.sol";
|
|
11
|
+
import {ICoin} from "../src/interfaces/ICoin.sol";
|
|
12
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
13
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
14
|
+
import {MockERC20} from "./mocks/MockERC20.sol";
|
|
15
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
16
|
+
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
17
|
+
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
|
|
18
|
+
|
|
19
|
+
/// @notice Tests for the launch fee feature (time-based dynamic fee)
|
|
20
|
+
/// @dev IMPORTANT: This test uses forge-config pragma to run in isolation mode, which properly
|
|
21
|
+
/// simulates transaction boundaries for transient storage testing.
|
|
22
|
+
contract LaunchFeeTest is BaseTest {
|
|
23
|
+
MockERC20 internal mockCurrency;
|
|
24
|
+
ContentCoin internal coin;
|
|
25
|
+
|
|
26
|
+
function setUp() public override {
|
|
27
|
+
super.setUpNonForked();
|
|
28
|
+
|
|
29
|
+
mockCurrency = new MockERC20("MockCurrency", "MCK");
|
|
30
|
+
|
|
31
|
+
// Fund the pool manager with backing currency
|
|
32
|
+
mockCurrency.mint(address(poolManager), 1_000_000_000 ether);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================
|
|
36
|
+
// Interface Support Tests
|
|
37
|
+
// ============================================
|
|
38
|
+
|
|
39
|
+
function test_coinSupportsIHasCreationInfo() public {
|
|
40
|
+
_deployCoin();
|
|
41
|
+
|
|
42
|
+
bool supported = IERC165(address(coin)).supportsInterface(type(IHasCreationInfo).interfaceId);
|
|
43
|
+
assertTrue(supported, "coin should support IHasCreationInfo");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// forge-config: default.isolate = true
|
|
47
|
+
function test_creationInfo_returnsCorrectValues() public {
|
|
48
|
+
uint256 deployTime = block.timestamp;
|
|
49
|
+
_deployCoin();
|
|
50
|
+
|
|
51
|
+
(uint256 creationTimestamp, bool isDeploying) = IHasCreationInfo(address(coin)).creationInfo();
|
|
52
|
+
|
|
53
|
+
assertEq(creationTimestamp, deployTime, "creation timestamp should match deploy time");
|
|
54
|
+
assertFalse(isDeploying, "isDeploying should be false after deployment transaction");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================
|
|
58
|
+
// Pool Key Tests
|
|
59
|
+
// ============================================
|
|
60
|
+
|
|
61
|
+
function test_poolKey_usesDynamicFeeFlag() public {
|
|
62
|
+
_deployCoin();
|
|
63
|
+
|
|
64
|
+
PoolKey memory poolKey = coin.getPoolKey();
|
|
65
|
+
|
|
66
|
+
assertEq(poolKey.fee, CoinConstants.DYNAMIC_FEE_FLAG, "pool fee should use DYNAMIC_FEE_FLAG");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================
|
|
70
|
+
// Launch Fee Calculation Tests
|
|
71
|
+
// ============================================
|
|
72
|
+
|
|
73
|
+
/// forge-config: default.isolate = true
|
|
74
|
+
function test_launchFee_immediatelyAfterCreation() public {
|
|
75
|
+
_deployCoin();
|
|
76
|
+
|
|
77
|
+
uint128 amountIn = 1 ether;
|
|
78
|
+
address trader = makeAddr("trader");
|
|
79
|
+
|
|
80
|
+
// Snapshot to compare swaps from same starting state
|
|
81
|
+
uint256 snapshot = vm.snapshotState();
|
|
82
|
+
|
|
83
|
+
// Swap immediately (same block, but different transaction)
|
|
84
|
+
// The launch fee should be at maximum (99%)
|
|
85
|
+
mockCurrency.mint(trader, amountIn);
|
|
86
|
+
uint256 coinBalanceBefore = coin.balanceOf(trader);
|
|
87
|
+
_swapCurrencyForCoin(amountIn, trader);
|
|
88
|
+
uint256 coinsAtMaxFee = coin.balanceOf(trader) - coinBalanceBefore;
|
|
89
|
+
|
|
90
|
+
console.log("Coins received at t=0 (99% fee):", coinsAtMaxFee);
|
|
91
|
+
|
|
92
|
+
// Revert to same starting state
|
|
93
|
+
vm.revertToState(snapshot);
|
|
94
|
+
|
|
95
|
+
// Warp past launch fee duration and do another swap from same starting state
|
|
96
|
+
vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION + 1);
|
|
97
|
+
|
|
98
|
+
mockCurrency.mint(trader, amountIn);
|
|
99
|
+
coinBalanceBefore = coin.balanceOf(trader);
|
|
100
|
+
_swapCurrencyForCoin(amountIn, trader);
|
|
101
|
+
uint256 coinsAtMinFee = coin.balanceOf(trader) - coinBalanceBefore;
|
|
102
|
+
|
|
103
|
+
console.log("Coins received at t>10s (1% fee):", coinsAtMinFee);
|
|
104
|
+
|
|
105
|
+
// Coins received with 1% fee should be significantly more than with 99% fee
|
|
106
|
+
assertGt(coinsAtMinFee, coinsAtMaxFee, "should receive more coins after launch fee ends");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// forge-config: default.isolate = true
|
|
110
|
+
function test_launchFee_decaysOverTime() public {
|
|
111
|
+
_deployCoin();
|
|
112
|
+
|
|
113
|
+
uint128 amountIn = 0.1 ether;
|
|
114
|
+
address trader = makeAddr("trader");
|
|
115
|
+
|
|
116
|
+
// Test at different time points
|
|
117
|
+
uint256[] memory timePoints = new uint256[](5);
|
|
118
|
+
timePoints[0] = 0; // 99% fee
|
|
119
|
+
timePoints[1] = 2; // ~79.2% fee
|
|
120
|
+
timePoints[2] = 5; // ~50% fee
|
|
121
|
+
timePoints[3] = 8; // ~20.8% fee
|
|
122
|
+
timePoints[4] = 10; // 1% fee
|
|
123
|
+
|
|
124
|
+
uint256[] memory coinsReceived = new uint256[](5);
|
|
125
|
+
|
|
126
|
+
for (uint256 i = 0; i < timePoints.length; i++) {
|
|
127
|
+
// Reset state for each test
|
|
128
|
+
uint256 snapshot = vm.snapshotState();
|
|
129
|
+
|
|
130
|
+
if (timePoints[i] > 0) {
|
|
131
|
+
vm.warp(block.timestamp + timePoints[i]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
mockCurrency.mint(trader, amountIn);
|
|
135
|
+
uint256 coinBalanceBefore = coin.balanceOf(trader);
|
|
136
|
+
|
|
137
|
+
_swapCurrencyForCoin(amountIn, trader);
|
|
138
|
+
|
|
139
|
+
coinsReceived[i] = coin.balanceOf(trader) - coinBalanceBefore;
|
|
140
|
+
|
|
141
|
+
console.log("Time:", timePoints[i], "s - Coins received:", coinsReceived[i]);
|
|
142
|
+
|
|
143
|
+
vm.revertToState(snapshot);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Verify monotonic increase (more coins as fee decreases)
|
|
147
|
+
for (uint256 i = 1; i < coinsReceived.length; i++) {
|
|
148
|
+
assertGt(coinsReceived[i], coinsReceived[i - 1], "coins received should increase as launch fee decays");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// forge-config: default.isolate = true
|
|
153
|
+
function test_launchFee_exactlyAtDuration() public {
|
|
154
|
+
_deployCoin();
|
|
155
|
+
|
|
156
|
+
uint128 amountIn = 0.1 ether;
|
|
157
|
+
address trader = makeAddr("trader");
|
|
158
|
+
|
|
159
|
+
// Test at exactly the launch fee duration
|
|
160
|
+
uint256 snapshot = vm.snapshotState();
|
|
161
|
+
vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION);
|
|
162
|
+
|
|
163
|
+
mockCurrency.mint(trader, amountIn);
|
|
164
|
+
uint256 coinBalanceBefore = coin.balanceOf(trader);
|
|
165
|
+
_swapCurrencyForCoin(amountIn, trader);
|
|
166
|
+
uint256 coinsAtExactDuration = coin.balanceOf(trader) - coinBalanceBefore;
|
|
167
|
+
|
|
168
|
+
vm.revertToState(snapshot);
|
|
169
|
+
|
|
170
|
+
// Test after the launch fee duration (same starting state)
|
|
171
|
+
vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION + 100);
|
|
172
|
+
|
|
173
|
+
mockCurrency.mint(trader, amountIn);
|
|
174
|
+
coinBalanceBefore = coin.balanceOf(trader);
|
|
175
|
+
_swapCurrencyForCoin(amountIn, trader);
|
|
176
|
+
uint256 coinsAfterDuration = coin.balanceOf(trader) - coinBalanceBefore;
|
|
177
|
+
|
|
178
|
+
// Should be approximately equal (both at 1% fee, same pool state)
|
|
179
|
+
assertApproxEqRel(coinsAtExactDuration, coinsAfterDuration, 0.01e18, "fee should be same at and after duration");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function test_launchFee_afterDurationEnds() public {
|
|
183
|
+
_deployCoin();
|
|
184
|
+
|
|
185
|
+
// Warp well past the launch fee duration
|
|
186
|
+
vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION + 1 days);
|
|
187
|
+
|
|
188
|
+
uint128 amountIn = 0.1 ether;
|
|
189
|
+
address trader = makeAddr("trader");
|
|
190
|
+
mockCurrency.mint(trader, amountIn);
|
|
191
|
+
|
|
192
|
+
// Should use normal 1% LP fee
|
|
193
|
+
_swapCurrencyForCoin(amountIn, trader);
|
|
194
|
+
|
|
195
|
+
// Just verify the swap succeeded - fee calculation is 1%
|
|
196
|
+
assertGt(coin.balanceOf(trader), 0, "trader should have received coins");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================
|
|
200
|
+
// Initial Supply Bypass Tests
|
|
201
|
+
// ============================================
|
|
202
|
+
|
|
203
|
+
function test_initialSupply_bypassesLaunchFee() public {
|
|
204
|
+
// The initial supply purchase during deployment should bypass launch fee
|
|
205
|
+
// This is verified by checking the creator receives coins during deployment
|
|
206
|
+
|
|
207
|
+
_deployCoin();
|
|
208
|
+
|
|
209
|
+
uint256 creatorBalanceAfter = coin.balanceOf(users.creator);
|
|
210
|
+
|
|
211
|
+
// Creator should receive initial supply (10 million for content coins)
|
|
212
|
+
assertEq(creatorBalanceAfter, CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY, "creator should receive full initial supply without launch fee");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ============================================
|
|
216
|
+
// Fee Calculation Math Tests
|
|
217
|
+
// ============================================
|
|
218
|
+
|
|
219
|
+
function test_feeCalculation_linearDecay() public pure {
|
|
220
|
+
// Test the fee calculation formula
|
|
221
|
+
// fee = startFee - (elapsed / duration) * (startFee - endFee)
|
|
222
|
+
|
|
223
|
+
uint256 startFee = CoinConstants.LAUNCH_FEE_START; // 990,000 (99%)
|
|
224
|
+
uint256 endFee = CoinConstants.LP_FEE_V4; // 10,000 (1%)
|
|
225
|
+
uint256 duration = CoinConstants.LAUNCH_FEE_DURATION; // 10 seconds
|
|
226
|
+
|
|
227
|
+
// At t=0: fee should be 990,000
|
|
228
|
+
uint256 feeAt0 = startFee - (0 * (startFee - endFee)) / duration;
|
|
229
|
+
assertEq(feeAt0, 990_000, "fee at t=0");
|
|
230
|
+
|
|
231
|
+
// At t=5: fee should be 500,000 (50%)
|
|
232
|
+
uint256 feeAt5 = startFee - (5 * (startFee - endFee)) / duration;
|
|
233
|
+
assertEq(feeAt5, 500_000, "fee at t=5");
|
|
234
|
+
|
|
235
|
+
// At t=10: fee should be 10,000 (1%)
|
|
236
|
+
uint256 feeAt10 = startFee - (10 * (startFee - endFee)) / duration;
|
|
237
|
+
assertEq(feeAt10, 10_000, "fee at t=10");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ============================================
|
|
241
|
+
// Helper Functions
|
|
242
|
+
// ============================================
|
|
243
|
+
|
|
244
|
+
function _deployCoin() internal {
|
|
245
|
+
bytes32 salt = keccak256(abi.encodePacked("launchFeeTest", block.timestamp));
|
|
246
|
+
bytes memory poolConfig = _defaultPoolConfig(address(mockCurrency));
|
|
247
|
+
|
|
248
|
+
vm.prank(users.creator);
|
|
249
|
+
(address coinAddress, ) = factory.deploy(
|
|
250
|
+
users.creator,
|
|
251
|
+
_getDefaultOwners(),
|
|
252
|
+
"https://test.com",
|
|
253
|
+
"LaunchFeeCoin",
|
|
254
|
+
"LAUNCH",
|
|
255
|
+
poolConfig,
|
|
256
|
+
address(0), // no platform referrer
|
|
257
|
+
address(0), // no post deploy hook
|
|
258
|
+
bytes(""),
|
|
259
|
+
salt
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
coin = ContentCoin(payable(coinAddress));
|
|
263
|
+
vm.label(address(coin), "LAUNCH_FEE_COIN");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function _swapCurrencyForCoin(uint128 amountIn, address trader) internal {
|
|
267
|
+
uint128 minAmountOut = 0;
|
|
268
|
+
|
|
269
|
+
(bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
|
|
270
|
+
address(mockCurrency),
|
|
271
|
+
amountIn,
|
|
272
|
+
address(coin),
|
|
273
|
+
minAmountOut,
|
|
274
|
+
coin.getPoolKey(),
|
|
275
|
+
bytes("")
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
vm.startPrank(trader);
|
|
279
|
+
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(mockCurrency), amountIn, uint48(block.timestamp + 1 days));
|
|
280
|
+
|
|
281
|
+
router.execute(commands, inputs, block.timestamp + 1 days);
|
|
282
|
+
vm.stopPrank();
|
|
283
|
+
}
|
|
284
|
+
}
|