@zoralabs/coins 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build$colon$js.log +99 -99
- package/CHANGELOG.md +44 -5
- package/README.md +4 -0
- package/abis/BaseCoin.json +0 -5
- package/abis/ContentCoin.json +0 -5
- package/abis/CreatorCoin.json +0 -5
- package/abis/FeeEstimatorHook.json +94 -1
- package/abis/IUpgradeableDestinationV4HookWithUpdateableFee.json +95 -0
- package/abis/IZoraFactory.json +69 -0
- package/abis/ZoraFactoryImpl.json +69 -0
- package/abis/ZoraV4CoinHook.json +94 -1
- package/addresses/8453.json +6 -6
- package/audits/report-cantinacode-zora-0827.pdf +3498 -4
- package/dist/index.cjs +21 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +54 -12
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/foundry.toml +3 -3
- package/package/wagmiGenerated.ts +21 -3
- package/package.json +1 -1
- package/script/TestBackingCoinSwap.s.sol +0 -2
- package/script/TestV4Swap.s.sol +0 -2
- package/src/BaseCoin.sol +4 -12
- package/src/ContentCoin.sol +3 -4
- package/src/CreatorCoin.sol +8 -10
- package/src/ZoraFactoryImpl.sol +115 -83
- package/src/hook-registry/ZoraHookRegistry.sol +4 -0
- package/src/hooks/ZoraV4CoinHook.sol +66 -9
- package/src/interfaces/IUpgradeableV4Hook.sol +18 -0
- package/src/interfaces/IZoraFactory.sol +21 -2
- package/src/libs/CoinConstants.sol +51 -8
- package/src/libs/CoinDopplerMultiCurve.sol +11 -11
- package/src/libs/CoinRewardsV4.sol +26 -33
- package/src/libs/CoinSetup.sol +2 -9
- package/src/libs/DopplerMath.sol +2 -2
- package/src/libs/V4Liquidity.sol +79 -15
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/Coin.t.sol +5 -5
- package/test/CoinRewardsV4.t.sol +33 -0
- package/test/CoinUniV4.t.sol +2 -3
- package/test/ContentCoinRewards.t.sol +43 -0
- package/test/CreatorCoin.t.sol +53 -29
- package/test/DeploymentHooks.t.sol +54 -2
- package/test/LiquidityMigration.t.sol +145 -7
- package/test/V4Liquidity.t.sol +178 -0
- package/test/utils/BaseTest.sol +0 -1
- package/test/utils/RewardTestHelpers.sol +4 -4
- package/abis/CoinConstants.json +0 -54
- package/abis/CoinRewardsV4.json +0 -67
- package/src/libs/CreatorCoinConstants.sol +0 -15
- package/src/libs/MarketConstants.sol +0 -23
- /package/abis/{VmContractHelper227.json → VmContractHelper226.json} +0 -0
package/src/libs/V4Liquidity.sol
CHANGED
|
@@ -28,8 +28,9 @@ import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
|
|
|
28
28
|
import {Position} from "@uniswap/v4-core/src/libraries/Position.sol";
|
|
29
29
|
import {BurnedPosition, Delta, MigratedLiquidityResult, IUpgradeableV4Hook} from "../interfaces/IUpgradeableV4Hook.sol";
|
|
30
30
|
import {PoolStateReader} from "../libs/PoolStateReader.sol";
|
|
31
|
-
import {IUpgradeableDestinationV4Hook} from "../interfaces/IUpgradeableV4Hook.sol";
|
|
31
|
+
import {IUpgradeableDestinationV4Hook, IUpgradeableDestinationV4HookWithUpdateableFee} from "../interfaces/IUpgradeableV4Hook.sol";
|
|
32
32
|
import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
|
|
33
|
+
import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
|
|
33
34
|
|
|
34
35
|
// command = 1; mint
|
|
35
36
|
struct MintCallbackData {
|
|
@@ -71,7 +72,7 @@ library V4Liquidity {
|
|
|
71
72
|
address coin,
|
|
72
73
|
address newHook,
|
|
73
74
|
bytes calldata additionalData
|
|
74
|
-
) internal returns (PoolKey memory) {
|
|
75
|
+
) internal returns (PoolKey memory newPoolKey) {
|
|
75
76
|
bytes memory data = abi.encode(
|
|
76
77
|
BURN_ALL_POSITIONS_CALLBACK_ID,
|
|
77
78
|
abi.encode(BurnAllPositionsCallbackData({poolKey: poolKey, positions: positions, coin: coin, newHook: newHook}))
|
|
@@ -82,19 +83,36 @@ library V4Liquidity {
|
|
|
82
83
|
|
|
83
84
|
MigratedLiquidityResult memory migratedLiquidityResult = abi.decode(result, (MigratedLiquidityResult));
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
newPoolKey.currency0 = poolKey.currency0;
|
|
87
|
+
newPoolKey.currency1 = poolKey.currency1;
|
|
88
|
+
newPoolKey.hooks = IHooks(newHook);
|
|
89
|
+
|
|
90
|
+
// Check if new hook supports the new interface first, then fall back to old interface
|
|
91
|
+
if (IERC165(newHook).supportsInterface(type(IUpgradeableDestinationV4HookWithUpdateableFee).interfaceId)) {
|
|
92
|
+
// Use new interface with fee updates
|
|
93
|
+
(uint24 fee, int24 tickSpacing) = IUpgradeableDestinationV4HookWithUpdateableFee(address(newHook)).initializeFromMigrationWithUpdateableFee(
|
|
94
|
+
poolKey,
|
|
95
|
+
coin,
|
|
96
|
+
migratedLiquidityResult.sqrtPriceX96,
|
|
97
|
+
migratedLiquidityResult.burnedPositions,
|
|
98
|
+
additionalData
|
|
99
|
+
);
|
|
100
|
+
newPoolKey.fee = fee;
|
|
101
|
+
newPoolKey.tickSpacing = tickSpacing;
|
|
102
|
+
} else {
|
|
103
|
+
// Fall back to old interface for backward compatibility
|
|
104
|
+
require(IERC165(newHook).supportsInterface(type(IUpgradeableDestinationV4Hook).interfaceId), IUpgradeableV4Hook.InvalidNewHook(newHook));
|
|
105
|
+
IUpgradeableDestinationV4Hook(address(newHook)).initializeFromMigration(
|
|
106
|
+
poolKey,
|
|
107
|
+
coin,
|
|
108
|
+
migratedLiquidityResult.sqrtPriceX96,
|
|
109
|
+
migratedLiquidityResult.burnedPositions,
|
|
110
|
+
additionalData
|
|
111
|
+
);
|
|
112
|
+
// Keep existing fee and tick spacing when using old interface
|
|
113
|
+
newPoolKey.fee = poolKey.fee;
|
|
114
|
+
newPoolKey.tickSpacing = poolKey.tickSpacing;
|
|
115
|
+
}
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
/// @notice Handles the callback from the pool manager. Called by the hook upon unlock.
|
|
@@ -138,6 +156,41 @@ library V4Liquidity {
|
|
|
138
156
|
return abi.encode(result);
|
|
139
157
|
}
|
|
140
158
|
|
|
159
|
+
function dedupePositions(LpPosition[] memory positions) internal pure returns (LpPosition[] memory dedupedPositions) {
|
|
160
|
+
// Upper bound: no more than input length
|
|
161
|
+
dedupedPositions = new LpPosition[](positions.length);
|
|
162
|
+
uint outLen = 0;
|
|
163
|
+
|
|
164
|
+
// O(n²) approach: for each position, check if it already exists in output
|
|
165
|
+
// This is acceptable since position arrays are typically small (< 100 positions)
|
|
166
|
+
|
|
167
|
+
for (uint i = 0; i < positions.length; i++) {
|
|
168
|
+
int24 t0 = positions[i].tickLower;
|
|
169
|
+
int24 t1 = positions[i].tickUpper;
|
|
170
|
+
uint128 v = positions[i].liquidity;
|
|
171
|
+
|
|
172
|
+
bool duplicate = false;
|
|
173
|
+
for (uint j = 0; j < outLen; j++) {
|
|
174
|
+
LpPosition memory dedupedPosition = dedupedPositions[j];
|
|
175
|
+
if (dedupedPosition.tickLower == t0 && dedupedPosition.tickUpper == t1) {
|
|
176
|
+
dedupedPosition.liquidity += v;
|
|
177
|
+
duplicate = true;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!duplicate) {
|
|
183
|
+
dedupedPositions[outLen] = LpPosition({tickLower: t0, tickUpper: t1, liquidity: v});
|
|
184
|
+
outLen++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Shrink to exact size by overwriting length field on the array
|
|
189
|
+
assembly {
|
|
190
|
+
mstore(dedupedPositions, outLen)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
141
194
|
function generatePositionsFromMigratedLiquidity(
|
|
142
195
|
uint160 sqrtPriceX96,
|
|
143
196
|
BurnedPosition[] calldata migratedLiquidity
|
|
@@ -198,6 +251,17 @@ library V4Liquidity {
|
|
|
198
251
|
for (uint256 i; i < positions.length; i++) {
|
|
199
252
|
uint128 liquidity = getLiquidity(poolManager, address(this), poolKey, positions[i].tickLower, positions[i].tickUpper);
|
|
200
253
|
|
|
254
|
+
// Skip positions that have no liquidity to avoid CannotUpdateEmptyPosition error
|
|
255
|
+
if (liquidity == 0) {
|
|
256
|
+
burnedPositions[i] = BurnedPosition({
|
|
257
|
+
tickLower: positions[i].tickLower,
|
|
258
|
+
tickUpper: positions[i].tickUpper,
|
|
259
|
+
amount0Received: 0,
|
|
260
|
+
amount1Received: 0
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
201
265
|
ModifyLiquidityParams memory params = ModifyLiquidityParams({
|
|
202
266
|
tickLower: positions[i].tickLower,
|
|
203
267
|
tickUpper: positions[i].tickUpper,
|
|
@@ -9,6 +9,6 @@ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersion
|
|
|
9
9
|
contract ContractVersionBase is IVersionedContract {
|
|
10
10
|
/// @notice The version of the contract
|
|
11
11
|
function contractVersion() external pure override returns (string memory) {
|
|
12
|
-
return "2.
|
|
12
|
+
return "2.3.0";
|
|
13
13
|
}
|
|
14
14
|
}
|
package/test/Coin.t.sol
CHANGED
|
@@ -37,16 +37,16 @@ contract CoinTest is BaseTest {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function test_supply_constants() public {
|
|
40
|
-
assertEq(CoinConstants.MAX_TOTAL_SUPPLY, CoinConstants.
|
|
40
|
+
assertEq(CoinConstants.MAX_TOTAL_SUPPLY, CoinConstants.CONTENT_COIN_MARKET_SUPPLY + CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY);
|
|
41
41
|
|
|
42
42
|
assertEq(CoinConstants.MAX_TOTAL_SUPPLY, 1_000_000_000e18);
|
|
43
|
-
assertEq(CoinConstants.
|
|
44
|
-
assertEq(CoinConstants.
|
|
43
|
+
assertEq(CoinConstants.CONTENT_COIN_MARKET_SUPPLY, 990_000_000e18);
|
|
44
|
+
assertEq(CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY, 10_000_000e18);
|
|
45
45
|
|
|
46
46
|
_deployV4Coin();
|
|
47
47
|
assertEq(coinV4.totalSupply(), CoinConstants.MAX_TOTAL_SUPPLY);
|
|
48
|
-
assertEq(coinV4.balanceOf(coinV4.payoutRecipient()), CoinConstants.
|
|
49
|
-
assertApproxEqAbs(coinV4.balanceOf(address(coinV4.poolManager())), CoinConstants.
|
|
48
|
+
assertEq(coinV4.balanceOf(coinV4.payoutRecipient()), CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY);
|
|
49
|
+
assertApproxEqAbs(coinV4.balanceOf(address(coinV4.poolManager())), CoinConstants.CONTENT_COIN_MARKET_SUPPLY, 1e18);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function test_initialize_validation() public {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
|
|
6
|
+
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
|
|
7
|
+
|
|
8
|
+
contract CoinRewardsV4Test is Test {
|
|
9
|
+
function test_convertDeltaToPositiveUint128_success_with_valid_positive_values() public pure {
|
|
10
|
+
// Test with small positive value
|
|
11
|
+
int256 smallDelta = 1000;
|
|
12
|
+
uint128 result = CoinRewardsV4.convertDeltaToPositiveUint128(smallDelta);
|
|
13
|
+
assertEq(result, uint128(uint256(smallDelta)));
|
|
14
|
+
|
|
15
|
+
// Test with large but valid positive value (within uint128 range)
|
|
16
|
+
int256 largeDelta = int256(uint256(type(uint128).max));
|
|
17
|
+
uint128 result2 = CoinRewardsV4.convertDeltaToPositiveUint128(largeDelta);
|
|
18
|
+
assertEq(result2, type(uint128).max);
|
|
19
|
+
|
|
20
|
+
// Test with zero
|
|
21
|
+
int256 zeroDelta = 0;
|
|
22
|
+
uint128 result3 = CoinRewardsV4.convertDeltaToPositiveUint128(zeroDelta);
|
|
23
|
+
assertEq(result3, 0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// forge-config: default.allow_internal_expect_revert = true
|
|
27
|
+
function test_convertDeltaToPositiveUint128_edge_cases_and_reverts(int8 difference) public {
|
|
28
|
+
if (difference < 0) {
|
|
29
|
+
vm.expectRevert(SafeCast.SafeCastOverflow.selector);
|
|
30
|
+
}
|
|
31
|
+
CoinRewardsV4.convertDeltaToPositiveUint128(difference);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/test/CoinUniV4.t.sol
CHANGED
|
@@ -17,7 +17,6 @@ import {LpPosition} from "../src/types/LpPosition.sol";
|
|
|
17
17
|
import {CoinCommon} from "../src/libs/CoinCommon.sol";
|
|
18
18
|
import {IZoraV4CoinHook} from "../src/interfaces/IZoraV4CoinHook.sol";
|
|
19
19
|
import {CoinConstants} from "../src/libs/CoinConstants.sol";
|
|
20
|
-
import {MarketConstants} from "../src/libs/MarketConstants.sol";
|
|
21
20
|
import {IMsgSender} from "../src/interfaces/IMsgSender.sol";
|
|
22
21
|
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
23
22
|
import {toBalanceDelta, BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
@@ -106,8 +105,8 @@ contract CoinUniV4Test is BaseTest {
|
|
|
106
105
|
|
|
107
106
|
// Verify total supply equals maximum allowed
|
|
108
107
|
assertEq(coinV4.totalSupply(), CoinConstants.MAX_TOTAL_SUPPLY, "total supply");
|
|
109
|
-
assertApproxEqAbs(coinV4.balanceOf(address(coinV4.poolManager())),
|
|
110
|
-
assertEq(coinV4.balanceOf(coinV4.payoutRecipient()), CoinConstants.
|
|
108
|
+
assertApproxEqAbs(coinV4.balanceOf(address(coinV4.poolManager())), CoinConstants.CONTENT_COIN_MARKET_SUPPLY, 1000, "pool launch supply");
|
|
109
|
+
assertEq(coinV4.balanceOf(coinV4.payoutRecipient()), CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY, "creator launch reward");
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
function test_estimateLpFees() public {
|
|
@@ -317,4 +317,47 @@ contract ContentCoinRewardsTest is BaseTest {
|
|
|
317
317
|
|
|
318
318
|
assertFalse(isLegacy, "Content coin should NOT be categorized as legacy creator coin");
|
|
319
319
|
}
|
|
320
|
+
|
|
321
|
+
/// @notice Test reward distribution when platform referrer rejects ETH - should fallback to protocol recipient
|
|
322
|
+
function test_rewards_platform_referrer_eth_rejection_fallback() public {
|
|
323
|
+
// Deploy ETH-rejecting contract to use as platform referrer
|
|
324
|
+
EthRejectingContract ethRejecter = new EthRejectingContract();
|
|
325
|
+
|
|
326
|
+
// Deploy ETH-backed coin (like test_distributesMarketRewardsInEth)
|
|
327
|
+
address currency = address(0); // ETH backing
|
|
328
|
+
bytes32 salt = keccak256(abi.encodePacked("eth-reject-test"));
|
|
329
|
+
_deployV4Coin(currency, address(ethRejecter), salt); // ethRejecter becomes platform referrer
|
|
330
|
+
|
|
331
|
+
// Fund trader with ETH
|
|
332
|
+
uint128 ethAmount = 0.1 ether;
|
|
333
|
+
address trader = makeAddr("trader");
|
|
334
|
+
deal(trader, ethAmount);
|
|
335
|
+
|
|
336
|
+
// Record initial ETH balances
|
|
337
|
+
uint256 initialProtocolEth = coinV4.protocolRewardRecipient().balance;
|
|
338
|
+
uint256 initialRejecterEth = address(ethRejecter).balance;
|
|
339
|
+
|
|
340
|
+
// Execute ETH -> Coin trade using the working BaseTest function
|
|
341
|
+
_swapSomeCurrencyForCoin(coinV4, currency, ethAmount, trader);
|
|
342
|
+
|
|
343
|
+
// Calculate ETH balance deltas
|
|
344
|
+
uint256 protocolEthDelta = coinV4.protocolRewardRecipient().balance - initialProtocolEth;
|
|
345
|
+
uint256 rejecterEthDelta = address(ethRejecter).balance - initialRejecterEth;
|
|
346
|
+
|
|
347
|
+
// Verify ETH-rejecting contract got no ETH
|
|
348
|
+
assertEq(rejecterEthDelta, 0, "Platform referrer should receive no ETH");
|
|
349
|
+
|
|
350
|
+
// Verify protocol got ETH (backup mechanism worked)
|
|
351
|
+
assertGt(protocolEthDelta, 0, "Protocol should receive backup ETH from failed platform referrer");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Contract that rejects ETH transfers (no payable functions)
|
|
356
|
+
contract EthRejectingContract {
|
|
357
|
+
// This contract has no receive() or fallback() payable functions
|
|
358
|
+
// so ETH transfers will fail
|
|
359
|
+
receive() external payable {
|
|
360
|
+
console.log("EthRejectingContract received ETH");
|
|
361
|
+
revert("EthRejectingContract received ETH");
|
|
362
|
+
}
|
|
320
363
|
}
|
package/test/CreatorCoin.t.sol
CHANGED
|
@@ -5,7 +5,7 @@ import "./utils/BaseTest.sol";
|
|
|
5
5
|
|
|
6
6
|
import {ICreatorCoin} from "../src/interfaces/ICreatorCoin.sol";
|
|
7
7
|
import {ICreatorCoinHook} from "../src/interfaces/ICreatorCoinHook.sol";
|
|
8
|
-
import {
|
|
8
|
+
import {CoinConstants} from "../src/libs/CoinConstants.sol";
|
|
9
9
|
import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
|
|
10
10
|
import {UniV4SwapHelper} from "../src/libs/UniV4SwapHelper.sol";
|
|
11
11
|
|
|
@@ -65,11 +65,11 @@ contract CreatorCoinTest is BaseTest {
|
|
|
65
65
|
assertEq(creatorCoin.name(), "Testcoin");
|
|
66
66
|
assertEq(creatorCoin.symbol(), "TEST");
|
|
67
67
|
assertEq(creatorCoin.payoutRecipient(), users.creator);
|
|
68
|
-
assertEq(creatorCoin.currency(),
|
|
69
|
-
assertEq(creatorCoin.totalSupply(),
|
|
68
|
+
assertEq(creatorCoin.currency(), CoinConstants.CREATOR_COIN_CURRENCY);
|
|
69
|
+
assertEq(creatorCoin.totalSupply(), CoinConstants.TOTAL_SUPPLY);
|
|
70
70
|
|
|
71
|
-
assertEq(creatorCoin.balanceOf(address(creatorCoin)),
|
|
72
|
-
assertEq(creatorCoin.balanceOf(address(creatorCoin.poolManager())),
|
|
71
|
+
assertEq(creatorCoin.balanceOf(address(creatorCoin)), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
|
|
72
|
+
assertEq(creatorCoin.balanceOf(address(creatorCoin.poolManager())), CoinConstants.CREATOR_COIN_MARKET_SUPPLY);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function test_deploy_creator_coin_with_invalid_currency_reverts() public {
|
|
@@ -105,7 +105,7 @@ contract CreatorCoinTest is BaseTest {
|
|
|
105
105
|
uint256 deploymentTime = block.timestamp;
|
|
106
106
|
|
|
107
107
|
assertEq(creatorCoin.vestingStartTime(), deploymentTime);
|
|
108
|
-
assertEq(creatorCoin.vestingEndTime(), deploymentTime +
|
|
108
|
+
assertEq(creatorCoin.vestingEndTime(), deploymentTime + CoinConstants.CREATOR_VESTING_DURATION);
|
|
109
109
|
assertEq(creatorCoin.totalClaimed(), 0);
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -124,16 +124,16 @@ contract CreatorCoinTest is BaseTest {
|
|
|
124
124
|
vm.warp(creatorCoin.vestingStartTime() + oneYear);
|
|
125
125
|
|
|
126
126
|
// After 1 year out of 5, should be able to claim 20% of vesting supply
|
|
127
|
-
uint256 expectedClaimable = (
|
|
127
|
+
uint256 expectedClaimable = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
|
|
128
128
|
assertEq(creatorCoin.getClaimableAmount(), expectedClaimable);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
function test_getClaimableAmount_after_half_vesting_period() public {
|
|
132
|
-
uint256 halfVesting =
|
|
132
|
+
uint256 halfVesting = CoinConstants.CREATOR_VESTING_DURATION / 2;
|
|
133
133
|
vm.warp(creatorCoin.vestingStartTime() + halfVesting);
|
|
134
134
|
|
|
135
135
|
// After 2.5 years, should be able to claim 50% of vesting supply
|
|
136
|
-
uint256 expectedClaimable =
|
|
136
|
+
uint256 expectedClaimable = CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 2;
|
|
137
137
|
assertEq(creatorCoin.getClaimableAmount(), expectedClaimable);
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -141,14 +141,14 @@ contract CreatorCoinTest is BaseTest {
|
|
|
141
141
|
vm.warp(creatorCoin.vestingEndTime());
|
|
142
142
|
|
|
143
143
|
// After full vesting period, should be able to claim entire vesting supply
|
|
144
|
-
assertEq(creatorCoin.getClaimableAmount(),
|
|
144
|
+
assertEq(creatorCoin.getClaimableAmount(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
function test_getClaimableAmount_after_vesting_period_ends() public {
|
|
148
148
|
vm.warp(creatorCoin.vestingEndTime() + 365 days);
|
|
149
149
|
|
|
150
150
|
// Even after vesting ends, should still be able to claim entire vesting supply
|
|
151
|
-
assertEq(creatorCoin.getClaimableAmount(),
|
|
151
|
+
assertEq(creatorCoin.getClaimableAmount(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
function test_getClaimableAmount_after_one_day() public {
|
|
@@ -156,12 +156,12 @@ contract CreatorCoinTest is BaseTest {
|
|
|
156
156
|
uint256 oneDay = 1 days;
|
|
157
157
|
vm.warp(creatorCoin.vestingStartTime() + oneDay);
|
|
158
158
|
|
|
159
|
-
uint256 expectedClaimable = (
|
|
159
|
+
uint256 expectedClaimable = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneDay) / CoinConstants.CREATOR_VESTING_DURATION;
|
|
160
160
|
assertEq(creatorCoin.getClaimableAmount(), expectedClaimable);
|
|
161
161
|
|
|
162
162
|
// Verify it's a small but non-zero amount
|
|
163
163
|
assertGt(expectedClaimable, 0);
|
|
164
|
-
assertLt(expectedClaimable,
|
|
164
|
+
assertLt(expectedClaimable, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 1000); // Less than 0.1%
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
function test_claimVesting_at_launch() public {
|
|
@@ -175,7 +175,7 @@ contract CreatorCoinTest is BaseTest {
|
|
|
175
175
|
uint256 oneYear = 365 days;
|
|
176
176
|
vm.warp(creatorCoin.vestingStartTime() + oneYear);
|
|
177
177
|
|
|
178
|
-
uint256 expectedClaimable = (
|
|
178
|
+
uint256 expectedClaimable = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
|
|
179
179
|
uint256 initialCreatorBalance = creatorCoin.balanceOf(users.creator);
|
|
180
180
|
uint256 initialContractBalance = creatorCoin.balanceOf(address(creatorCoin));
|
|
181
181
|
|
|
@@ -201,7 +201,7 @@ contract CreatorCoinTest is BaseTest {
|
|
|
201
201
|
|
|
202
202
|
// First claim after 1 year
|
|
203
203
|
vm.warp(creatorCoin.vestingStartTime() + oneYear);
|
|
204
|
-
uint256 expectedClaim1 = (
|
|
204
|
+
uint256 expectedClaim1 = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
|
|
205
205
|
uint256 claimed1 = creatorCoin.claimVesting();
|
|
206
206
|
|
|
207
207
|
assertEq(claimed1, expectedClaim1);
|
|
@@ -210,7 +210,7 @@ contract CreatorCoinTest is BaseTest {
|
|
|
210
210
|
|
|
211
211
|
// Second claim after another year (2 years total)
|
|
212
212
|
vm.warp(creatorCoin.vestingStartTime() + 2 * oneYear);
|
|
213
|
-
uint256 totalVestedAfter2Years = (
|
|
213
|
+
uint256 totalVestedAfter2Years = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * 2 * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
|
|
214
214
|
uint256 expectedClaim2 = totalVestedAfter2Years - expectedClaim1;
|
|
215
215
|
|
|
216
216
|
uint256 claimed2 = creatorCoin.claimVesting();
|
|
@@ -241,9 +241,9 @@ contract CreatorCoinTest is BaseTest {
|
|
|
241
241
|
|
|
242
242
|
uint256 claimedAmount = creatorCoin.claimVesting();
|
|
243
243
|
|
|
244
|
-
assertEq(claimedAmount,
|
|
245
|
-
assertEq(creatorCoin.totalClaimed(),
|
|
246
|
-
assertEq(creatorCoin.balanceOf(users.creator),
|
|
244
|
+
assertEq(claimedAmount, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
|
|
245
|
+
assertEq(creatorCoin.totalClaimed(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
|
|
246
|
+
assertEq(creatorCoin.balanceOf(users.creator), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
|
|
247
247
|
|
|
248
248
|
// Subsequent claims should return 0
|
|
249
249
|
uint256 secondClaim = creatorCoin.claimVesting();
|
|
@@ -251,21 +251,21 @@ contract CreatorCoinTest is BaseTest {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
function test_claimVesting_partial_then_full() public {
|
|
254
|
-
uint256 halfVesting =
|
|
254
|
+
uint256 halfVesting = CoinConstants.CREATOR_VESTING_DURATION / 2;
|
|
255
255
|
|
|
256
256
|
// Claim half way through vesting
|
|
257
257
|
vm.warp(creatorCoin.vestingStartTime() + halfVesting);
|
|
258
258
|
uint256 partialClaim = creatorCoin.claimVesting();
|
|
259
|
-
assertEq(partialClaim,
|
|
259
|
+
assertEq(partialClaim, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 2);
|
|
260
260
|
|
|
261
261
|
// Claim the rest after full vesting
|
|
262
262
|
vm.warp(creatorCoin.vestingEndTime());
|
|
263
263
|
uint256 remainingClaim = creatorCoin.claimVesting();
|
|
264
|
-
assertEq(remainingClaim,
|
|
264
|
+
assertEq(remainingClaim, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 2);
|
|
265
265
|
|
|
266
266
|
// Total should equal full vesting supply
|
|
267
|
-
assertEq(creatorCoin.totalClaimed(),
|
|
268
|
-
assertEq(creatorCoin.balanceOf(users.creator),
|
|
267
|
+
assertEq(creatorCoin.totalClaimed(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
|
|
268
|
+
assertEq(creatorCoin.balanceOf(users.creator), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
function test_vesting_calculation_edge_cases() public {
|
|
@@ -277,17 +277,17 @@ contract CreatorCoinTest is BaseTest {
|
|
|
277
277
|
vm.warp(creatorCoin.vestingStartTime() + 1);
|
|
278
278
|
uint256 claimableAfterOneSecond = creatorCoin.getClaimableAmount();
|
|
279
279
|
assertGt(claimableAfterOneSecond, 0);
|
|
280
|
-
assertLt(claimableAfterOneSecond,
|
|
280
|
+
assertLt(claimableAfterOneSecond, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 1000000); // Very small amount
|
|
281
281
|
|
|
282
282
|
// Test one second before vesting ends
|
|
283
283
|
vm.warp(creatorCoin.vestingEndTime() - 1);
|
|
284
284
|
uint256 claimableBeforeEnd = creatorCoin.getClaimableAmount();
|
|
285
|
-
assertLt(claimableBeforeEnd,
|
|
286
|
-
assertGt(claimableBeforeEnd,
|
|
285
|
+
assertLt(claimableBeforeEnd, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
|
|
286
|
+
assertGt(claimableBeforeEnd, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY - (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 1000000));
|
|
287
287
|
|
|
288
288
|
// Test at exact vesting end time
|
|
289
289
|
vm.warp(creatorCoin.vestingEndTime());
|
|
290
|
-
assertEq(creatorCoin.getClaimableAmount(),
|
|
290
|
+
assertEq(creatorCoin.getClaimableAmount(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
function test_vesting_frequent_small_claims() public {
|
|
@@ -302,11 +302,35 @@ contract CreatorCoinTest is BaseTest {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
// Verify total claimed matches expected amount for 7 days
|
|
305
|
-
uint256 expectedTotal = (
|
|
305
|
+
uint256 expectedTotal = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * 7 days) / CoinConstants.CREATOR_VESTING_DURATION;
|
|
306
306
|
assertEq(totalClaimed, expectedTotal);
|
|
307
307
|
assertEq(creatorCoin.totalClaimed(), expectedTotal);
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
function test_vesting_duration_accounts_for_leap_years() public pure {
|
|
311
|
+
// Verify the vesting duration is exactly 5 years accounting for leap years
|
|
312
|
+
// 365.25 days per year * 5 years = 1826.25 days = 157,788,000 seconds
|
|
313
|
+
uint256 expectedDuration = 5 * 365.25 days;
|
|
314
|
+
uint256 expectedSeconds = 157_788_000; // 5 * 365.25 * 24 * 60 * 60
|
|
315
|
+
|
|
316
|
+
assertEq(CoinConstants.CREATOR_VESTING_DURATION, expectedDuration);
|
|
317
|
+
assertEq(CoinConstants.CREATOR_VESTING_DURATION, expectedSeconds);
|
|
318
|
+
|
|
319
|
+
// Verify it's longer than 5 * 365 days (which would be the old incorrect duration)
|
|
320
|
+
uint256 oldIncorrectDuration = 5 * 365 days;
|
|
321
|
+
uint256 differenceInSeconds = expectedDuration - oldIncorrectDuration;
|
|
322
|
+
uint256 expectedDifferenceInDays = 1.25 days; // 1.25 days = 108,000 seconds
|
|
323
|
+
|
|
324
|
+
assertEq(differenceInSeconds, expectedDifferenceInDays);
|
|
325
|
+
assertEq(differenceInSeconds, 108_000); // 1.25 * 24 * 60 * 60
|
|
326
|
+
|
|
327
|
+
// Verify this matches exactly 5 years with leap year correction
|
|
328
|
+
// Over 5 years, there's typically 1 leap day (Feb 29), plus 0.25 day per year
|
|
329
|
+
// for the quarter-day that accumulates: 1 + (5 * 0.25) = 2.25 days total
|
|
330
|
+
// But we use 365.25 average, so: 5 * 0.25 = 1.25 additional days
|
|
331
|
+
assertTrue(CoinConstants.CREATOR_VESTING_DURATION > oldIncorrectDuration);
|
|
332
|
+
}
|
|
333
|
+
|
|
310
334
|
function test_buy(uint128 amountIn) public {
|
|
311
335
|
vm.assume(amountIn > 0.00001e18);
|
|
312
336
|
vm.assume(amountIn < 500_000e18);
|
|
@@ -4,6 +4,7 @@ pragma solidity ^0.8.13;
|
|
|
4
4
|
import {BaseTest} from "./utils/BaseTest.sol";
|
|
5
5
|
import {BuySupplyWithSwapRouterHook} from "../src/hooks/deployment/BuySupplyWithSwapRouterHook.sol";
|
|
6
6
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
7
|
+
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
7
8
|
import {IUniswapV3Pool} from "../src/interfaces/IUniswapV3Pool.sol";
|
|
8
9
|
import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
|
|
9
10
|
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
@@ -64,7 +65,7 @@ contract DeploymentsHooksTest is BaseTest {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function test_buySupplyWithEthUsingV4Hook_withExactInputMultiHop(uint256 initialOrderSize) public {
|
|
67
|
-
vm.assume(initialOrderSize >
|
|
68
|
+
vm.assume(initialOrderSize > 0.0001 ether);
|
|
68
69
|
vm.assume(initialOrderSize < 1 ether);
|
|
69
70
|
|
|
70
71
|
vm.deal(users.creator, initialOrderSize);
|
|
@@ -105,7 +106,7 @@ contract DeploymentsHooksTest is BaseTest {
|
|
|
105
106
|
assertEq(coinV4.currency(), zora, "currency");
|
|
106
107
|
assertGt(amountCurrency, 0, "amountCurrency > 0");
|
|
107
108
|
assertGt(coinsPurchased, 0, "coinsPurchased > 0");
|
|
108
|
-
assertEq(coinV4.balanceOf(users.creator), CoinConstants.
|
|
109
|
+
assertEq(coinV4.balanceOf(users.creator), CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY + coinsPurchased, "balanceOf creator");
|
|
109
110
|
// assertGt(IERC20(zora).balanceOf(address(pool)), 0, "Pool ZORA balance");
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -211,4 +212,55 @@ contract DeploymentsHooksTest is BaseTest {
|
|
|
211
212
|
vm.prank(users.creator);
|
|
212
213
|
_deployWithHook(address(0), bytes(""), zora);
|
|
213
214
|
}
|
|
215
|
+
|
|
216
|
+
function test_creatorCoin_deployWithHook_buySupplyWithEth() public {
|
|
217
|
+
uint256 initialOrderSize = 0.0001 ether;
|
|
218
|
+
vm.deal(users.creator, initialOrderSize);
|
|
219
|
+
|
|
220
|
+
uint24 poolFee = 3000;
|
|
221
|
+
|
|
222
|
+
bytes memory hookData = _encodeExactInputSingle(
|
|
223
|
+
users.creator,
|
|
224
|
+
ISwapRouter.ExactInputSingleParams({
|
|
225
|
+
tokenIn: address(weth),
|
|
226
|
+
tokenOut: zora,
|
|
227
|
+
fee: poolFee,
|
|
228
|
+
recipient: address(buySupplyWithSwapRouterHook),
|
|
229
|
+
amountIn: initialOrderSize,
|
|
230
|
+
amountOutMinimum: 0,
|
|
231
|
+
sqrtPriceLimitX96: 0
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(zora);
|
|
236
|
+
|
|
237
|
+
vm.prank(users.creator);
|
|
238
|
+
(address creatorCoinAddress, bytes memory hookDataOut) = factory.deployCreatorCoin{value: initialOrderSize}(
|
|
239
|
+
users.creator,
|
|
240
|
+
_getDefaultOwners(),
|
|
241
|
+
"https://test.com",
|
|
242
|
+
"Creator Coin Test",
|
|
243
|
+
"CCT",
|
|
244
|
+
poolConfig,
|
|
245
|
+
users.platformReferrer,
|
|
246
|
+
address(buySupplyWithSwapRouterHook),
|
|
247
|
+
hookData,
|
|
248
|
+
bytes32(uint256(123)) // coinSalt
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
(uint256 amountCurrency, uint256 coinsPurchased) = abi.decode(hookDataOut, (uint256, uint256));
|
|
252
|
+
|
|
253
|
+
// Verify the creator coin was deployed successfully
|
|
254
|
+
ICoin creatorCoin = ICoin(creatorCoinAddress);
|
|
255
|
+
assertEq(creatorCoin.currency(), zora, "currency should be ZORA");
|
|
256
|
+
assertEq(IERC20Metadata(creatorCoinAddress).name(), "Creator Coin Test", "name should match");
|
|
257
|
+
assertEq(IERC20Metadata(creatorCoinAddress).symbol(), "CCT", "symbol should match");
|
|
258
|
+
|
|
259
|
+
// Verify the hook executed and purchased coins
|
|
260
|
+
assertGt(amountCurrency, 0, "amountCurrency should be > 0");
|
|
261
|
+
assertGt(coinsPurchased, 0, "coinsPurchased should be > 0");
|
|
262
|
+
|
|
263
|
+
// Verify creator received the purchased coins (launch reward vests over time for creator coins)
|
|
264
|
+
assertEq(IERC20(creatorCoinAddress).balanceOf(users.creator), coinsPurchased, "creator should have purchased coins");
|
|
265
|
+
}
|
|
214
266
|
}
|