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