@zoralabs/coins 2.3.0 → 2.3.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 +116 -97
- package/CHANGELOG.md +7 -1
- package/README.md +1 -0
- package/abis/AddressConstants.json +7 -0
- package/abis/BaseTest.json +62 -0
- package/abis/BuySupplyWithV4SwapHook.json +429 -0
- package/abis/IUniswapV4Router04.json +484 -0
- package/abis/MockAirlock.json +39 -0
- package/abis/SimpleERC20.json +326 -0
- package/addresses/8453.json +7 -9
- package/audits/report-cantinacode-zora-1021.pdf +0 -0
- package/dist/index.cjs +140 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +139 -18
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +205 -28
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/package/wagmiGenerated.ts +139 -18
- package/package.json +1 -1
- package/script/DeployPostDeploymentHooks.s.sol +1 -3
- package/src/deployment/CoinsDeployerBase.sol +9 -8
- package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +310 -0
- package/src/utils/AutoSwapper.sol +1 -1
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/BuySupplyWithV4SwapHook.t.sol +509 -0
- package/test/Coin.t.sol +21 -9
- package/test/CoinUniV4.t.sol +1 -2
- package/test/ContentCoinRewards.t.sol +1 -3
- package/test/CreatorCoin.t.sol +1 -4
- package/test/CreatorCoinRewards.t.sol +1 -3
- package/test/Factory.t.sol +3 -3
- package/test/MultiOwnable.t.sol +4 -4
- package/test/Upgrades.t.sol +26 -17
- package/test/ZoraHookRegistry.t.sol +19 -9
- package/test/mocks/MockAirlock.sol +22 -0
- package/test/mocks/SimpleERC20.sol +8 -0
- package/test/utils/BaseTest.sol +155 -2
- package/test/utils/hookmate/README.md +50 -0
- package/test/utils/hookmate/artifacts/DeployHelper.sol +20 -0
- package/test/utils/hookmate/artifacts/Permit2.sol +16 -0
- package/test/utils/hookmate/artifacts/UniversalRouter.sol +29 -0
- package/test/utils/hookmate/artifacts/V4PoolManager.sol +17 -0
- package/test/utils/hookmate/artifacts/V4PositionManager.sol +23 -0
- package/test/utils/hookmate/artifacts/V4Quoter.sol +17 -0
- package/test/utils/hookmate/artifacts/V4Router.sol +18 -0
- package/test/utils/hookmate/constants/AddressConstants.sol +193 -0
- package/test/utils/hookmate/interfaces/router/IUniswapV4Router04.sol +173 -0
- package/test/utils/hookmate/interfaces/router/PathKey.sol +34 -0
- package/test/utils/hookmate/test/utils/SwapFeeEventAsserter.sol +24 -0
- package/wagmi.config.ts +1 -1
- package/src/utils/uniswap/BytesLib.sol +0 -35
- package/src/utils/uniswap/Path.sol +0 -31
- /package/abis/{VmContractHelper226.json → VmContractHelper239.json} +0 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import {BaseTest} from "./utils/BaseTest.sol";
|
|
5
|
+
import {BuySupplyWithV4SwapHook} from "../src/hooks/deployment/BuySupplyWithV4SwapHook.sol";
|
|
6
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
7
|
+
import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
|
|
8
|
+
import {ICoin} from "../src/interfaces/ICoin.sol";
|
|
9
|
+
import {ISwapRouter} from "../src/interfaces/ISwapRouter.sol";
|
|
10
|
+
import {CoinConstants} from "../src/libs/CoinConstants.sol";
|
|
11
|
+
import {ContentCoin} from "../src/ContentCoin.sol";
|
|
12
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
13
|
+
import {console} from "forge-std/console.sol";
|
|
14
|
+
|
|
15
|
+
contract BuySupplyWithV4SwapHookTest is BaseTest {
|
|
16
|
+
address constant ZORA = 0x1111111111166b7FE7bd91427724B487980aFc69;
|
|
17
|
+
BuySupplyWithV4SwapHook postDeployHook;
|
|
18
|
+
|
|
19
|
+
// TODO: Add tests to verify swap path always goes from input currency to backing currency
|
|
20
|
+
// 1. Test V3-only swap paths (e.g., USDC -> WETH -> Creator Coin)
|
|
21
|
+
// 2. Test V4-only swap paths (e.g., WETH -> Creator Coin via V4)
|
|
22
|
+
// 3. Test mixed V3->V4 swap paths (e.g., USDC -> WETH via V3, then WETH -> Creator Coin via V4)
|
|
23
|
+
// 4. Test different input currencies (USDC, WETH, other ERC20s) all properly route to backing currency
|
|
24
|
+
// 5. Verify the final currency received always matches the Content Coin's backing currency
|
|
25
|
+
// 6. Clean up debug logging in BuySupplyWithV4SwapHook.sol
|
|
26
|
+
|
|
27
|
+
function setUp() public override {
|
|
28
|
+
super.setUpWithBlockNumber(33646532);
|
|
29
|
+
|
|
30
|
+
postDeployHook = new BuySupplyWithV4SwapHook(factory, address(swapRouter), address(V4_POOL_MANAGER));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function _encodeV4HookData(
|
|
34
|
+
address buyRecipient,
|
|
35
|
+
bytes memory v3Route,
|
|
36
|
+
PoolKey[] memory v4Route,
|
|
37
|
+
address inputCurrency,
|
|
38
|
+
uint256 inputAmount,
|
|
39
|
+
uint256 minAmountOut
|
|
40
|
+
) internal pure returns (bytes memory) {
|
|
41
|
+
BuySupplyWithV4SwapHook.InitialSupplyParams memory params = BuySupplyWithV4SwapHook.InitialSupplyParams({
|
|
42
|
+
buyRecipient: buyRecipient,
|
|
43
|
+
v3Route: v3Route,
|
|
44
|
+
v4Route: v4Route,
|
|
45
|
+
inputCurrency: inputCurrency,
|
|
46
|
+
inputAmount: inputAmount,
|
|
47
|
+
minAmountOut: minAmountOut
|
|
48
|
+
});
|
|
49
|
+
return abi.encode(params);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _encodeV3Path(address tokenA, uint24 feeA, address tokenB, uint24 feeB, address tokenC) internal pure returns (bytes memory) {
|
|
53
|
+
return abi.encodePacked(tokenA, feeA, tokenB, feeB, tokenC);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _encodeV3PathSingle(address tokenA, uint24 fee, address tokenB) internal pure returns (bytes memory) {
|
|
57
|
+
return abi.encodePacked(tokenA, fee, tokenB);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function _deployCreatorCoin(address payoutRecipient) internal returns (address creatorCoinAddress) {
|
|
61
|
+
bytes memory creatorPoolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(ZORA);
|
|
62
|
+
|
|
63
|
+
vm.prank(payoutRecipient);
|
|
64
|
+
creatorCoinAddress = factory.deployCreatorCoin(
|
|
65
|
+
payoutRecipient, // payoutRecipient
|
|
66
|
+
_getDefaultOwners(), // owners
|
|
67
|
+
"https://creator.com", // uri
|
|
68
|
+
"Creator Coin", // name
|
|
69
|
+
"CREATOR", // symbol
|
|
70
|
+
creatorPoolConfig, // poolConfig (ZORA-backed)
|
|
71
|
+
users.platformReferrer, // platformReferrer
|
|
72
|
+
bytes32(0) // coinSalt
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _deployContentCoinWithHook(
|
|
77
|
+
address backingCurrency,
|
|
78
|
+
uint256 payableAmount,
|
|
79
|
+
address caller,
|
|
80
|
+
bytes memory hookData
|
|
81
|
+
) internal returns (address coinAddress, uint256 amountCurrency, uint256 coinsPurchased) {
|
|
82
|
+
bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(backingCurrency);
|
|
83
|
+
|
|
84
|
+
vm.prank(caller);
|
|
85
|
+
bytes memory hookDataOut;
|
|
86
|
+
(coinAddress, hookDataOut) = factory.deployWithHook{value: payableAmount}(
|
|
87
|
+
caller, // payoutRecipient
|
|
88
|
+
_getDefaultOwners(), // owners
|
|
89
|
+
"https://test.com", // uri
|
|
90
|
+
"Content Coin", // name
|
|
91
|
+
"CONTENT", // symbol
|
|
92
|
+
poolConfig, // poolConfig
|
|
93
|
+
users.platformReferrer, // platformReferrer
|
|
94
|
+
address(postDeployHook), // postDeployHook
|
|
95
|
+
hookData // postDeployHookData
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
(amountCurrency, coinsPurchased) = abi.decode(hookDataOut, (uint256, uint256));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// @dev Test buying initial supply of a Content Coin backed by ZORA
|
|
102
|
+
/// This only requires V3 swap (ETH -> ZORA) since the coin is already backed by ZORA
|
|
103
|
+
function test_buyContentCoinSupply_V3SwapOnly() public {
|
|
104
|
+
uint256 initialOrderSize = 0.1 ether;
|
|
105
|
+
vm.deal(users.creator, initialOrderSize);
|
|
106
|
+
|
|
107
|
+
// Create V3 path: ETH -> USDC -> ZORA
|
|
108
|
+
bytes memory v3Route = _encodeV3Path(
|
|
109
|
+
address(weth),
|
|
110
|
+
3000, // WETH/USDC 0.3%
|
|
111
|
+
USDC_ADDRESS,
|
|
112
|
+
3000, // USDC/ZORA 0.3%
|
|
113
|
+
ZORA
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
console.logBytes(v3Route);
|
|
117
|
+
|
|
118
|
+
// No V4 route needed since coin is backed by ZORA
|
|
119
|
+
PoolKey[] memory v4Route = new PoolKey[](0);
|
|
120
|
+
|
|
121
|
+
bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), initialOrderSize, 0);
|
|
122
|
+
|
|
123
|
+
// Deploy Content Coin backed by ZORA
|
|
124
|
+
(address coinAddress, uint256 amountCurrency, uint256 coinsPurchased) = _deployContentCoinWithHook(ZORA, initialOrderSize, users.creator, hookData);
|
|
125
|
+
|
|
126
|
+
ContentCoin coin = ContentCoin(payable(coinAddress));
|
|
127
|
+
|
|
128
|
+
// Verify the coin is properly configured
|
|
129
|
+
assertEq(coin.currency(), ZORA, "Coin should be backed by ZORA");
|
|
130
|
+
assertGt(amountCurrency, 0, "Should have received ZORA from V3 swap");
|
|
131
|
+
assertGt(coinsPurchased, 0, "Should have purchased coins");
|
|
132
|
+
|
|
133
|
+
// Creator should have their launch supply + purchased coins
|
|
134
|
+
assertEq(
|
|
135
|
+
coin.balanceOf(users.creator),
|
|
136
|
+
CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY + coinsPurchased,
|
|
137
|
+
"Creator should have launch supply + purchased coins"
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Verify V3 swap worked correctly (mock implementation returns positive values)
|
|
141
|
+
// Note: In real implementation this would check actual pool liquidity
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// @dev Test that BuyInitialSupply event is emitted with accurate data using snapshot pattern
|
|
145
|
+
function test_BuyInitialSupplyEvent() public {
|
|
146
|
+
uint256 initialOrderSize = 0.1 ether;
|
|
147
|
+
vm.deal(users.creator, initialOrderSize * 2); // Double to account for both runs
|
|
148
|
+
|
|
149
|
+
// Create V3 path: ETH -> USDC -> ZORA
|
|
150
|
+
bytes memory v3Route = _encodeV3Path(
|
|
151
|
+
address(weth),
|
|
152
|
+
3000, // WETH/USDC 0.3%
|
|
153
|
+
USDC_ADDRESS,
|
|
154
|
+
3000, // USDC/ZORA 0.3%
|
|
155
|
+
ZORA
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// No V4 route needed since coin is backed by ZORA
|
|
159
|
+
PoolKey[] memory v4Route = new PoolKey[](0);
|
|
160
|
+
|
|
161
|
+
bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), initialOrderSize, 0);
|
|
162
|
+
|
|
163
|
+
PoolKey[] memory expectedV4Route = new PoolKey[](1);
|
|
164
|
+
|
|
165
|
+
// FIRST RUN: Use snapshot pattern to capture expected values
|
|
166
|
+
uint256 snapshot = vm.snapshot();
|
|
167
|
+
|
|
168
|
+
// Execute deployment to get actual values
|
|
169
|
+
(address coinAddress, uint256 expectedAmountCurrency, uint256 expectedCoinsPurchased) = _deployContentCoinWithHook(
|
|
170
|
+
ZORA,
|
|
171
|
+
initialOrderSize,
|
|
172
|
+
users.creator,
|
|
173
|
+
hookData
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expectedV4Route[0] = ICoin(payable(coinAddress)).getPoolKey();
|
|
177
|
+
|
|
178
|
+
// Revert to snapshot to restore state
|
|
179
|
+
vm.revertToState(snapshot);
|
|
180
|
+
|
|
181
|
+
// SECOND RUN: Execute with event verification using captured values
|
|
182
|
+
// Note: We skip checking coin address (first indexed param) since it will be different after snapshot revert
|
|
183
|
+
vm.expectEmit(false, true, true, true); // Skip coin address, check recipient, coinsPurchased, and all data
|
|
184
|
+
emit BuySupplyWithV4SwapHook.BuyInitialSupply(
|
|
185
|
+
address(0), // coin (indexed) - skip checking since address will differ after revert
|
|
186
|
+
users.creator, // recipient (indexed)
|
|
187
|
+
expectedCoinsPurchased, // coinsPurchased (indexed)
|
|
188
|
+
v3Route, // v3Route (data)
|
|
189
|
+
expectedV4Route, // v4Route (data)
|
|
190
|
+
address(0), // inputCurrency (data) - ETH represented as address(0)
|
|
191
|
+
initialOrderSize, // inputAmount (data)
|
|
192
|
+
expectedAmountCurrency // v4SwapInput (data) - amount received from V3 swap
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Deploy Content Coin backed by ZORA - this should emit event with matching parameters
|
|
196
|
+
_deployContentCoinWithHook(ZORA, initialOrderSize, users.creator, hookData);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// @dev Test buying initial supply of a Content Coin paired with ETH
|
|
200
|
+
/// This requires no V3 or V4 routing - just direct V4 swap with ETH
|
|
201
|
+
function test_buyContentCoinSupply_ETHPaired() public {
|
|
202
|
+
uint256 initialOrderSize = 0.05 ether;
|
|
203
|
+
vm.deal(users.creator, initialOrderSize);
|
|
204
|
+
|
|
205
|
+
// No V3 route needed - direct ETH to coin swap
|
|
206
|
+
bytes memory v3Route = "";
|
|
207
|
+
|
|
208
|
+
// No V4 route needed - direct swap
|
|
209
|
+
PoolKey[] memory v4Route = new PoolKey[](0);
|
|
210
|
+
|
|
211
|
+
bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), initialOrderSize, 0);
|
|
212
|
+
|
|
213
|
+
// Deploy Content Coin paired with ETH (address(0))
|
|
214
|
+
(address coinAddress, uint256 amountCurrency, uint256 coinsPurchased) = _deployContentCoinWithHook(
|
|
215
|
+
address(0),
|
|
216
|
+
initialOrderSize,
|
|
217
|
+
users.creator,
|
|
218
|
+
hookData
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
ContentCoin coin = ContentCoin(payable(coinAddress));
|
|
222
|
+
|
|
223
|
+
// Verify the coin is properly configured as ETH-paired
|
|
224
|
+
assertEq(coin.currency(), address(0), "Coin should be paired with ETH");
|
|
225
|
+
assertEq(amountCurrency, initialOrderSize, "Should have used all ETH directly");
|
|
226
|
+
assertGt(coinsPurchased, 0, "Should have purchased coins");
|
|
227
|
+
|
|
228
|
+
// Creator should have their launch reward + purchased coins
|
|
229
|
+
assertEq(
|
|
230
|
+
coin.balanceOf(users.creator),
|
|
231
|
+
CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY + coinsPurchased,
|
|
232
|
+
"Creator should have launch reward + purchased coins"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// @dev Test deploying Content Coin with owned Creator Coin tokens (no ETH, no V3 swap)
|
|
237
|
+
/// This demonstrates using existing ERC20 tokens to purchase initial supply during deployment
|
|
238
|
+
function test_buyContentCoinSupply_WithOwnedCreatorCoins() public {
|
|
239
|
+
// STEP 1: Deploy Creator Coin backed by ZORA
|
|
240
|
+
address creatorCoinAddress = _deployCreatorCoin(users.creator);
|
|
241
|
+
|
|
242
|
+
// STEP 2: Give another user ZORA tokens and have them swap for Creator Coins
|
|
243
|
+
address anotherCreator = makeAddr("anotherCreator");
|
|
244
|
+
uint256 zoraAmount = 10e18; // 10 ZORA tokens
|
|
245
|
+
deal(ZORA, anotherCreator, zoraAmount);
|
|
246
|
+
assertEq(IERC20(ZORA).balanceOf(anotherCreator), zoraAmount, "anotherCreator should have ZORA tokens");
|
|
247
|
+
|
|
248
|
+
// Swap ZORA tokens for Creator Coins using proper V4 swap mechanism
|
|
249
|
+
uint128 swapAmountIn = uint128(zoraAmount);
|
|
250
|
+
_swapSomeCurrencyForCoin(ICoin(payable(creatorCoinAddress)), ZORA, swapAmountIn, anotherCreator);
|
|
251
|
+
|
|
252
|
+
uint256 creatorCoinAmount = IERC20(creatorCoinAddress).balanceOf(anotherCreator);
|
|
253
|
+
|
|
254
|
+
// STEP 3: Have anotherCreator approve the hook to spend their Creator Coins
|
|
255
|
+
vm.prank(anotherCreator);
|
|
256
|
+
IERC20(creatorCoinAddress).approve(address(postDeployHook), creatorCoinAmount);
|
|
257
|
+
|
|
258
|
+
// STEP 4: Deploy Content Coin backed by Creator Coin using owned tokens
|
|
259
|
+
|
|
260
|
+
// No V3 route needed - anotherCreator already has Creator Coins
|
|
261
|
+
bytes memory v3Route = "";
|
|
262
|
+
|
|
263
|
+
// No V4 route needed - direct Creator Coin to Content Coin swap
|
|
264
|
+
PoolKey[] memory v4Route = new PoolKey[](0);
|
|
265
|
+
|
|
266
|
+
bytes memory hookData = _encodeV4HookData(anotherCreator, v3Route, v4Route, creatorCoinAddress, creatorCoinAmount, 0);
|
|
267
|
+
|
|
268
|
+
// Deploy with amount = 0 (no ETH needed since using owned tokens)
|
|
269
|
+
(address contentCoinAddress, uint256 amountCurrency, uint256 coinsPurchased) = _deployContentCoinWithHook(
|
|
270
|
+
creatorCoinAddress,
|
|
271
|
+
0, // No ETH needed
|
|
272
|
+
anotherCreator,
|
|
273
|
+
hookData
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
ContentCoin contentCoin = ContentCoin(payable(contentCoinAddress));
|
|
277
|
+
|
|
278
|
+
// Verify the content coin is properly configured
|
|
279
|
+
assertEq(contentCoin.currency(), creatorCoinAddress, "Content coin should be backed by Creator coin");
|
|
280
|
+
assertGt(amountCurrency, 0, "Should have used some Creator Coins");
|
|
281
|
+
assertGt(coinsPurchased, 0, "Should have purchased content coins");
|
|
282
|
+
|
|
283
|
+
// anotherCreator should have their launch reward + purchased content coins
|
|
284
|
+
assertEq(
|
|
285
|
+
contentCoin.balanceOf(anotherCreator),
|
|
286
|
+
CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY + coinsPurchased,
|
|
287
|
+
"anotherCreator should have launch reward + purchased content coins"
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Verify Creator Coin balance decreased
|
|
291
|
+
uint256 remainingCreatorCoins = IERC20(creatorCoinAddress).balanceOf(anotherCreator);
|
|
292
|
+
assertLt(remainingCreatorCoins, creatorCoinAmount, "anotherCreator should have spent some Creator Coins");
|
|
293
|
+
assertEq(remainingCreatorCoins, creatorCoinAmount - amountCurrency, "Creator Coin balance should decrease by amount used");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// @dev Test buying initial supply of a Content Coin backed by a Creator Coin
|
|
297
|
+
/// This requires V3 swap (ETH -> ZORA) then V4 swap (ZORA -> Creator Coin -> Content Coin)
|
|
298
|
+
function test_buyContentCoinSupply_CreatorCoinBacked() public {
|
|
299
|
+
uint256 initialOrderSize = 0.08 ether;
|
|
300
|
+
vm.deal(users.creator, initialOrderSize);
|
|
301
|
+
|
|
302
|
+
// STEP 1: Deploy Creator Coin backed by ZORA
|
|
303
|
+
address creatorCoinAddress = _deployCreatorCoin(users.creator);
|
|
304
|
+
|
|
305
|
+
// STEP 2: Deploy Content Coin backed by Creator Coin
|
|
306
|
+
|
|
307
|
+
// Create V3 path: ETH -> USDC -> ZORA (to get the creator coin's backing currency)
|
|
308
|
+
bytes memory v3Route = _encodeV3Path(
|
|
309
|
+
address(weth),
|
|
310
|
+
3000, // WETH/USDC 0.3%
|
|
311
|
+
USDC_ADDRESS,
|
|
312
|
+
3000, // USDC/ZORA 0.3%
|
|
313
|
+
ZORA
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// V4 route: ZORA -> Creator Coin (then Creator Coin -> Content Coin will be added automatically)
|
|
317
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
318
|
+
v4Route[0] = ICoin(payable(creatorCoinAddress)).getPoolKey();
|
|
319
|
+
|
|
320
|
+
bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), initialOrderSize, 0);
|
|
321
|
+
|
|
322
|
+
(address contentCoinAddress, uint256 amountCurrency, uint256 coinsPurchased) = _deployContentCoinWithHook(
|
|
323
|
+
creatorCoinAddress,
|
|
324
|
+
initialOrderSize,
|
|
325
|
+
users.creator,
|
|
326
|
+
hookData
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
ContentCoin contentCoin = ContentCoin(payable(contentCoinAddress));
|
|
330
|
+
|
|
331
|
+
// Verify the content coin is properly configured
|
|
332
|
+
assertEq(contentCoin.currency(), creatorCoinAddress, "Content coin should be backed by Creator coin");
|
|
333
|
+
assertGt(amountCurrency, 0, "Should have received ZORA from V3 swap");
|
|
334
|
+
assertGt(coinsPurchased, 0, "Should have purchased content coins");
|
|
335
|
+
|
|
336
|
+
// Creator should have their launch reward + purchased content coins
|
|
337
|
+
assertEq(
|
|
338
|
+
contentCoin.balanceOf(users.creator),
|
|
339
|
+
CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY + coinsPurchased,
|
|
340
|
+
"Creator should have launch reward + purchased content coins"
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============ ERROR HANDLING TESTS ============
|
|
345
|
+
|
|
346
|
+
function test_RevertWhen_InsufficientInputCurrencyETH() public {
|
|
347
|
+
uint256 inputAmount = 1 ether;
|
|
348
|
+
uint256 insufficientAmount = 0.5 ether;
|
|
349
|
+
|
|
350
|
+
// Create V3 path: ETH -> USDC -> ZORA
|
|
351
|
+
bytes memory v3Route = _encodeV3Path(
|
|
352
|
+
address(weth),
|
|
353
|
+
3000, // WETH/USDC 0.3%
|
|
354
|
+
USDC_ADDRESS,
|
|
355
|
+
3000, // USDC/ZORA 0.3%
|
|
356
|
+
ZORA
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
PoolKey[] memory v4Route = new PoolKey[](0);
|
|
360
|
+
|
|
361
|
+
bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), inputAmount, 0);
|
|
362
|
+
|
|
363
|
+
// Should revert with InsufficientInputCurrency
|
|
364
|
+
vm.deal(users.creator, insufficientAmount);
|
|
365
|
+
bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(ZORA);
|
|
366
|
+
vm.expectRevert(abi.encodeWithSelector(BuySupplyWithV4SwapHook.InsufficientInputCurrency.selector, inputAmount, insufficientAmount));
|
|
367
|
+
|
|
368
|
+
vm.prank(users.creator);
|
|
369
|
+
factory.deployWithHook{value: insufficientAmount}(
|
|
370
|
+
users.creator, // payoutRecipient
|
|
371
|
+
_getDefaultOwners(), // owners
|
|
372
|
+
"https://test.com", // uri
|
|
373
|
+
"Content Coin", // name
|
|
374
|
+
"CONTENT", // symbol
|
|
375
|
+
poolConfig, // poolConfig
|
|
376
|
+
users.platformReferrer, // platformReferrer
|
|
377
|
+
address(postDeployHook), // postDeployHook
|
|
378
|
+
hookData // postDeployHookData
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function test_RevertWhen_InsufficientInputCurrencyERC20() public {
|
|
383
|
+
// Deploy Creator Coin first
|
|
384
|
+
address creatorCoinAddress = _deployCreatorCoin(users.creator);
|
|
385
|
+
|
|
386
|
+
uint256 userBalance = 500e18;
|
|
387
|
+
|
|
388
|
+
// Give user some Creator Coins but less than required
|
|
389
|
+
deal(creatorCoinAddress, users.creator, userBalance);
|
|
390
|
+
|
|
391
|
+
// Approve a small amount to spend for Creator Coins
|
|
392
|
+
vm.prank(users.creator);
|
|
393
|
+
IERC20(creatorCoinAddress).approve(address(postDeployHook), 1);
|
|
394
|
+
|
|
395
|
+
// No V3 route needed - user already has Creator Coins (but insufficient amount)
|
|
396
|
+
bytes memory v3Route = "";
|
|
397
|
+
PoolKey[] memory v4Route = new PoolKey[](0);
|
|
398
|
+
|
|
399
|
+
uint256 zoraAmount = 10e18; // 10 ZORA tokens
|
|
400
|
+
deal(ZORA, users.creator, zoraAmount);
|
|
401
|
+
|
|
402
|
+
// Swap ZORA tokens for Creator Coins using proper V4 swap mechanism
|
|
403
|
+
uint128 swapAmountIn = uint128(zoraAmount);
|
|
404
|
+
_swapSomeCurrencyForCoin(ICoin(payable(creatorCoinAddress)), ZORA, swapAmountIn, users.creator);
|
|
405
|
+
|
|
406
|
+
uint256 inputAmount = IERC20(creatorCoinAddress).balanceOf(users.creator);
|
|
407
|
+
uint256 amountToApprove = inputAmount / 2;
|
|
408
|
+
|
|
409
|
+
// only approve half of the input amount - it should revert
|
|
410
|
+
vm.prank(users.creator);
|
|
411
|
+
IERC20(creatorCoinAddress).approve(address(postDeployHook), amountToApprove);
|
|
412
|
+
|
|
413
|
+
bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, creatorCoinAddress, inputAmount, 0);
|
|
414
|
+
|
|
415
|
+
bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(creatorCoinAddress);
|
|
416
|
+
// Should revert with InsufficientInputCurrency
|
|
417
|
+
vm.expectRevert(abi.encodeWithSelector(BuySupplyWithV4SwapHook.InsufficientInputCurrency.selector, inputAmount, amountToApprove));
|
|
418
|
+
|
|
419
|
+
vm.prank(users.creator);
|
|
420
|
+
factory.deployWithHook(
|
|
421
|
+
users.creator, // payoutRecipient
|
|
422
|
+
_getDefaultOwners(), // owners
|
|
423
|
+
"https://test.com", // uri
|
|
424
|
+
"Content Coin", // name
|
|
425
|
+
"CONTENT", // symbol
|
|
426
|
+
poolConfig, // poolConfig
|
|
427
|
+
users.platformReferrer, // platformReferrer
|
|
428
|
+
address(postDeployHook), // postDeployHook
|
|
429
|
+
hookData // postDeployHookData
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function test_RevertWhen_V3RouteDoesNotConnectToV4RouteStart() public {
|
|
434
|
+
// Deploy Creator Coin backed by ZORA
|
|
435
|
+
address creatorCoinAddress = _deployCreatorCoin(users.creator);
|
|
436
|
+
|
|
437
|
+
vm.deal(users.creator, 1 ether);
|
|
438
|
+
|
|
439
|
+
// Create V3 path that ends with USDC
|
|
440
|
+
bytes memory v3Route = _encodeV3PathSingle(
|
|
441
|
+
address(weth),
|
|
442
|
+
3000, // WETH/USDC 0.3%
|
|
443
|
+
USDC_ADDRESS
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// Create V4 route that starts with ZORA (not USDC - mismatch!)
|
|
447
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
448
|
+
v4Route[0] = ICoin(payable(creatorCoinAddress)).getPoolKey();
|
|
449
|
+
|
|
450
|
+
bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), 1 ether, 0);
|
|
451
|
+
|
|
452
|
+
// Should revert with V3RouteDoesNotConnectToV4RouteStart
|
|
453
|
+
|
|
454
|
+
bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(creatorCoinAddress);
|
|
455
|
+
|
|
456
|
+
vm.prank(users.creator);
|
|
457
|
+
vm.expectRevert(abi.encodeWithSelector(BuySupplyWithV4SwapHook.V3RouteDoesNotConnectToV4RouteStart.selector));
|
|
458
|
+
factory.deployWithHook{value: 1 ether}(
|
|
459
|
+
users.creator, // payoutRecipient
|
|
460
|
+
_getDefaultOwners(), // owners
|
|
461
|
+
"https://test.com", // uri
|
|
462
|
+
"Content Coin", // name
|
|
463
|
+
"CONTENT", // symbol
|
|
464
|
+
poolConfig, // poolConfig
|
|
465
|
+
users.platformReferrer, // platformReferrer
|
|
466
|
+
address(postDeployHook), // postDeployHook
|
|
467
|
+
hookData // postDeployHookData
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function test_RevertWhen_InsufficientOutputAmount() public {
|
|
472
|
+
uint256 initialOrderSize = 0.1 ether;
|
|
473
|
+
vm.deal(users.creator, initialOrderSize);
|
|
474
|
+
|
|
475
|
+
// Create V3 path: ETH -> USDC -> ZORA
|
|
476
|
+
bytes memory v3Route = _encodeV3Path(
|
|
477
|
+
address(weth),
|
|
478
|
+
3000, // WETH/USDC 0.3%
|
|
479
|
+
USDC_ADDRESS,
|
|
480
|
+
3000, // USDC/ZORA 0.3%
|
|
481
|
+
ZORA
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// No V4 route needed since coin is backed by ZORA
|
|
485
|
+
PoolKey[] memory v4Route = new PoolKey[](0);
|
|
486
|
+
|
|
487
|
+
// Set impossibly high minimum amount out (1 million coins)
|
|
488
|
+
uint256 impossibleMinAmountOut = type(uint256).max;
|
|
489
|
+
|
|
490
|
+
bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), initialOrderSize, impossibleMinAmountOut);
|
|
491
|
+
|
|
492
|
+
// Should revert with InsufficientOutputAmount
|
|
493
|
+
bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(ZORA);
|
|
494
|
+
vm.expectRevert(abi.encodeWithSelector(BuySupplyWithV4SwapHook.InsufficientOutputAmount.selector));
|
|
495
|
+
|
|
496
|
+
vm.prank(users.creator);
|
|
497
|
+
factory.deployWithHook{value: initialOrderSize}(
|
|
498
|
+
users.creator, // payoutRecipient
|
|
499
|
+
_getDefaultOwners(), // owners
|
|
500
|
+
"https://test.com", // uri
|
|
501
|
+
"Content Coin", // name
|
|
502
|
+
"CONTENT", // symbol
|
|
503
|
+
poolConfig, // poolConfig
|
|
504
|
+
users.platformReferrer, // platformReferrer
|
|
505
|
+
address(postDeployHook), // postDeployHook
|
|
506
|
+
hookData // postDeployHookData
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
package/test/Coin.t.sol
CHANGED
|
@@ -16,7 +16,7 @@ contract CoinTest is BaseTest {
|
|
|
16
16
|
using stdJson for string;
|
|
17
17
|
|
|
18
18
|
function setUp() public override {
|
|
19
|
-
super.
|
|
19
|
+
super.setUpNonForked();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function test_contract_ierc165_support() public {
|
|
@@ -107,23 +107,35 @@ contract CoinTest is BaseTest {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function test_burn() public {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
// Deploy a mock ERC20 currency
|
|
111
|
+
MockERC20 mockCurrency = new MockERC20("Mock Currency", "MOCK");
|
|
112
|
+
mockCurrency.mint(users.buyer, 1000 ether);
|
|
113
|
+
// Pool manager needs currency for liquidity operations
|
|
114
|
+
mockCurrency.mint(address(poolManager), 1000000 ether);
|
|
115
|
+
|
|
116
|
+
// Deploy coin with mock currency
|
|
117
|
+
coinV4 = ContentCoin(payable(address(_deployV4Coin(address(mockCurrency), address(0), bytes32(0)))));
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
// Approve with permit2 and swap
|
|
120
|
+
uint128 swapAmount = 1 ether;
|
|
121
|
+
vm.startPrank(users.buyer);
|
|
122
|
+
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(mockCurrency), swapAmount, uint48(block.timestamp + 1 days));
|
|
123
|
+
vm.stopPrank();
|
|
124
|
+
|
|
125
|
+
_swapSomeCurrencyForCoin(coinV4, address(mockCurrency), swapAmount, users.buyer);
|
|
126
|
+
|
|
127
|
+
uint256 beforeBalance = coinV4.balanceOf(users.buyer);
|
|
116
128
|
uint256 beforeTotalSupply = coinV4.totalSupply();
|
|
117
129
|
|
|
118
130
|
uint256 burnAmount = beforeBalance / 2;
|
|
119
131
|
|
|
120
|
-
vm.prank(users.
|
|
132
|
+
vm.prank(users.buyer);
|
|
121
133
|
coinV4.burn(burnAmount);
|
|
122
134
|
|
|
123
|
-
uint256 afterBalance = coinV4.balanceOf(users.
|
|
135
|
+
uint256 afterBalance = coinV4.balanceOf(users.buyer);
|
|
124
136
|
uint256 afterTotalSupply = coinV4.totalSupply();
|
|
125
137
|
|
|
126
|
-
assertEq(beforeBalance - afterBalance, burnAmount, "
|
|
138
|
+
assertEq(beforeBalance - afterBalance, burnAmount, "buyer coin balance");
|
|
127
139
|
assertEq(beforeTotalSupply - afterTotalSupply, burnAmount, "coin total supply");
|
|
128
140
|
}
|
|
129
141
|
|
package/test/CoinUniV4.t.sol
CHANGED
|
@@ -38,9 +38,8 @@ contract CoinUniV4Test is BaseTest {
|
|
|
38
38
|
MockERC20 internal mockERC20B;
|
|
39
39
|
|
|
40
40
|
function setUp() public override {
|
|
41
|
-
super.
|
|
41
|
+
super.setUpNonForked();
|
|
42
42
|
|
|
43
|
-
quoter = IV4Quoter(V4_QUOTER);
|
|
44
43
|
mockERC20A = new MockERC20("MockERC20A", "MCKA");
|
|
45
44
|
mockERC20B = new MockERC20("MockERC20B", "MCKB");
|
|
46
45
|
|
|
@@ -21,9 +21,7 @@ contract ContentCoinRewardsTest is BaseTest {
|
|
|
21
21
|
address internal tradeReferrer;
|
|
22
22
|
|
|
23
23
|
function setUp() public override {
|
|
24
|
-
super.
|
|
25
|
-
|
|
26
|
-
deal(address(zoraToken), address(poolManager), 1_000_000_000e18);
|
|
24
|
+
super.setUpNonForked();
|
|
27
25
|
|
|
28
26
|
backingCreatorCoin = CreatorCoin(_deployCreatorCoin());
|
|
29
27
|
|
package/test/CreatorCoin.t.sol
CHANGED
|
@@ -13,10 +13,7 @@ contract CreatorCoinTest is BaseTest {
|
|
|
13
13
|
CreatorCoin internal creatorCoin;
|
|
14
14
|
|
|
15
15
|
function setUp() public override {
|
|
16
|
-
super.
|
|
17
|
-
|
|
18
|
-
deal(address(zoraToken), address(poolManager), 1_000_000_000e18);
|
|
19
|
-
|
|
16
|
+
super.setUpNonForked();
|
|
20
17
|
_deployCreatorCoin();
|
|
21
18
|
}
|
|
22
19
|
|
|
@@ -22,9 +22,7 @@ contract CreatorCoinRewardsTest is BaseTest {
|
|
|
22
22
|
address internal tradeReferrer;
|
|
23
23
|
|
|
24
24
|
function setUp() public override {
|
|
25
|
-
super.
|
|
26
|
-
|
|
27
|
-
deal(address(zoraToken), address(poolManager), 1_000_000_000e18);
|
|
25
|
+
super.setUpNonForked();
|
|
28
26
|
|
|
29
27
|
// Set up referrer addresses for all tests
|
|
30
28
|
platformReferrer = makeAddr("platformReferrer");
|
package/test/Factory.t.sol
CHANGED
|
@@ -8,7 +8,7 @@ import {IZoraFactory} from "../src/interfaces/IZoraFactory.sol";
|
|
|
8
8
|
|
|
9
9
|
contract FactoryTest is BaseTest {
|
|
10
10
|
function setUp() public override {
|
|
11
|
-
super.
|
|
11
|
+
super.setUpNonForked();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function test_factory_constructor_and_proxy_setup() public {
|
|
@@ -115,8 +115,8 @@ contract FactoryTest is BaseTest {
|
|
|
115
115
|
|
|
116
116
|
address platformReferrer = users.platformReferrer;
|
|
117
117
|
|
|
118
|
-
bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(address(
|
|
119
|
-
bytes memory poolConfigForGettingAddress = poolConfigChanged ? CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(
|
|
118
|
+
bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(address(0));
|
|
119
|
+
bytes memory poolConfigForGettingAddress = poolConfigChanged ? CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(ZORA_TOKEN_ADDRESS) : poolConfig;
|
|
120
120
|
|
|
121
121
|
address expectedCoinAddress = factory.coinAddress(msgSender, name, symbol, poolConfigForGettingAddress, platformReferrer, salt);
|
|
122
122
|
|
package/test/MultiOwnable.t.sol
CHANGED
|
@@ -5,7 +5,7 @@ import "./utils/BaseTest.sol";
|
|
|
5
5
|
|
|
6
6
|
contract MultiOwnableTest is BaseTest {
|
|
7
7
|
function setUp() public override {
|
|
8
|
-
super.
|
|
8
|
+
super.setUpNonForked();
|
|
9
9
|
|
|
10
10
|
_deployV4Coin();
|
|
11
11
|
}
|
|
@@ -135,7 +135,7 @@ contract MultiOwnableTest is BaseTest {
|
|
|
135
135
|
|
|
136
136
|
function test_revert_init_with_zero_owners() public {
|
|
137
137
|
address[] memory emptyOwners = new address[](0);
|
|
138
|
-
bytes memory poolConfig_ = _generatePoolConfig(address(
|
|
138
|
+
bytes memory poolConfig_ = _generatePoolConfig(address(0));
|
|
139
139
|
vm.expectRevert(MultiOwnable.OneOwnerRequired.selector);
|
|
140
140
|
factory.deploy(users.creator, emptyOwners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
|
|
141
141
|
}
|
|
@@ -143,7 +143,7 @@ contract MultiOwnableTest is BaseTest {
|
|
|
143
143
|
function test_revert_init_with_zero_address() public {
|
|
144
144
|
address[] memory owners = new address[](1);
|
|
145
145
|
owners[0] = address(0);
|
|
146
|
-
bytes memory poolConfig_ = _generatePoolConfig(address(
|
|
146
|
+
bytes memory poolConfig_ = _generatePoolConfig(address(0));
|
|
147
147
|
vm.expectRevert(MultiOwnable.OwnerCannotBeAddressZero.selector);
|
|
148
148
|
factory.deploy(users.creator, owners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
|
|
149
149
|
}
|
|
@@ -152,7 +152,7 @@ contract MultiOwnableTest is BaseTest {
|
|
|
152
152
|
address[] memory owners = new address[](2);
|
|
153
153
|
owners[0] = users.creator;
|
|
154
154
|
owners[1] = users.creator;
|
|
155
|
-
bytes memory poolConfig_ = _generatePoolConfig(address(
|
|
155
|
+
bytes memory poolConfig_ = _generatePoolConfig(address(0));
|
|
156
156
|
vm.expectRevert(MultiOwnable.AlreadyOwner.selector);
|
|
157
157
|
factory.deploy(users.creator, owners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
|
|
158
158
|
}
|