@zoralabs/coins 0.7.1 → 1.0.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.log +106 -84
- package/CHANGELOG.md +68 -0
- package/abis/BadImpl.json +15 -0
- package/abis/BalanceDeltaLibrary.json +15 -0
- package/abis/BaseCoin.json +1350 -0
- package/abis/BaseCoinDeployHook.json +78 -0
- package/abis/BaseHook.json +897 -0
- package/abis/BaseTest.json +60 -91
- package/abis/BeforeSwapDeltaLibrary.json +15 -0
- package/abis/BuySupplyWithSwapRouterHook.json +126 -0
- package/abis/Coin.json +214 -150
- package/abis/CoinConstants.json +65 -0
- package/abis/CoinDopplerMultiCurve.json +38 -0
- package/abis/CoinRewardsV4.json +54 -0
- package/abis/CoinTest.json +66 -111
- package/abis/CoinUniV4Test.json +1053 -0
- package/abis/CoinV4.json +1687 -0
- package/abis/CurrencyLibrary.json +25 -0
- package/abis/DeployHooks.json +9 -0
- package/abis/DeployScript.json +47 -0
- package/abis/DeployedCoinVersionLookup.json +21 -0
- package/abis/DeployedCoinVersionLookupTest.json +716 -0
- package/abis/DifferentNamespaceVersionLookup.json +39 -0
- package/abis/DopplerUniswapV3Test.json +62 -184
- package/abis/ERC20.json +310 -0
- package/abis/FactoryTest.json +98 -98
- package/abis/FakeHookNoInterface.json +21 -0
- package/abis/FeeEstimatorHook.json +1528 -0
- package/abis/Hooks.json +28 -0
- package/abis/HooksDeployment.json +23 -0
- package/abis/HooksTest.json +698 -0
- package/abis/IAllowanceTransfer.json +486 -0
- package/abis/ICoin.json +62 -69
- package/abis/ICoinDeployHook.json +31 -0
- package/abis/ICoinV3.json +879 -0
- package/abis/ICoinV4.json +915 -0
- package/abis/IContractMetadata.json +28 -0
- package/abis/IDeployedCoinVersionLookup.json +21 -0
- package/abis/IEIP712.json +15 -0
- package/abis/IEIP712_v4.json +15 -0
- package/abis/IERC20Minimal.json +172 -0
- package/abis/IERC6909Claims.json +288 -0
- package/abis/IERC721.json +36 -36
- package/abis/IERC721Permit_v4.json +88 -0
- package/abis/IExtsload.json +64 -0
- package/abis/IExttload.json +40 -0
- package/abis/IHasAfterCoinDeploy.json +31 -0
- package/abis/IHasContractName.json +15 -0
- package/abis/IHasPoolKey.json +42 -0
- package/abis/IHasRewardsRecipients.json +54 -0
- package/abis/IHasSwapPath.json +60 -0
- package/abis/IHooks.json +789 -0
- package/abis/IImmutableState.json +15 -0
- package/abis/IMsgSender.json +15 -0
- package/abis/IMulticall_v4.json +21 -0
- package/abis/INotifier.json +187 -0
- package/abis/IPermit2.json +865 -0
- package/abis/IPermit2Forwarder.json +138 -0
- package/abis/IPoolConfigEncoding.json +46 -0
- package/abis/IPoolInitializer_v4.json +53 -0
- package/abis/IPoolManager.json +1286 -0
- package/abis/IPositionManager.json +712 -0
- package/abis/IProtocolFees.json +174 -0
- package/abis/ISignatureTransfer.json +394 -0
- package/abis/ISubscriber.json +89 -0
- package/abis/ISwapPathRouter.json +92 -0
- package/abis/ISwapRouter.json +82 -0
- package/abis/IUniversalRouter.json +61 -0
- package/abis/IUnlockCallback.json +21 -0
- package/abis/IUnorderedNonce.json +44 -0
- package/abis/IV4Quoter.json +310 -0
- package/abis/IV4Router.json +47 -0
- package/abis/IZoraFactory.json +328 -4
- package/abis/IZoraV4CoinHook.json +427 -0
- package/abis/ImmutableState.json +36 -0
- package/abis/LPFeeLibrary.json +65 -0
- package/abis/MockERC20.json +21 -0
- package/abis/MultiOwnableTest.json +60 -91
- package/abis/{CoinConfigurationVersions.json → Position.json} +1 -1
- package/abis/PrintUpgradeCommand.json +9 -0
- package/abis/ProxyShim.json +24 -0
- package/abis/Simulate.json +0 -91
- package/abis/StateLibrary.json +80 -0
- package/abis/TestDeployedCoinVersionLookupImplementation.json +39 -0
- package/abis/TestV4Swap.json +9 -0
- package/abis/{CoinSetup.json → UniV3BuySell.json} +5 -0
- package/abis/UniV3Errors.json +32 -0
- package/abis/UpgradeCoinImpl.json +47 -0
- package/abis/UpgradeFactoryImpl.json +9 -0
- package/abis/UpgradesTest.json +671 -0
- package/abis/Vm.json +1482 -111
- package/abis/VmSafe.json +856 -32
- package/abis/ZoraFactoryImpl.json +450 -1
- package/abis/ZoraV4CoinHook.json +1439 -0
- package/addresses/8453.json +8 -3
- package/addresses/84532.json +8 -3
- package/dist/index.cjs +1998 -184
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1989 -178
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +2852 -688
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/package/wagmiGenerated.ts +1992 -173
- package/package.json +7 -2
- package/remappings.txt +6 -1
- package/script/CoinsDeployerBase.sol +105 -10
- package/script/DeployDevFactory.s.sol +21 -0
- package/script/DeployHooks.s.sol +22 -0
- package/script/PrintUpgradeCommand.s.sol +13 -0
- package/script/Simulate.s.sol +4 -12
- package/script/TestBackingCoinSwap.s.sol +146 -0
- package/script/TestV4Swap.s.sol +136 -0
- package/script/UpgradeCoinImpl.sol +2 -2
- package/script/UpgradeFactoryImpl.s.sol +23 -0
- package/src/BaseCoin.sol +176 -0
- package/src/Coin.sol +93 -515
- package/src/CoinV4.sol +121 -0
- package/src/ZoraFactoryImpl.sol +257 -57
- package/src/hooks/ZoraV4CoinHook.sol +195 -0
- package/src/hooks/deployment/BaseCoinDeployHook.sol +62 -0
- package/src/hooks/deployment/BuySupplyWithSwapRouterHook.sol +80 -0
- package/src/interfaces/ICoin.sol +35 -39
- package/src/interfaces/ICoinDeployHook.sol +8 -0
- package/src/interfaces/ICoinV3.sol +71 -0
- package/src/interfaces/ICoinV4.sol +69 -0
- package/src/interfaces/IDeployedCoinVersionLookup.sol +11 -0
- package/src/interfaces/IMsgSender.sol +9 -0
- package/src/interfaces/IPoolConfigEncoding.sol +14 -0
- package/src/interfaces/ISwapPathRouter.sol +14 -0
- package/src/interfaces/ISwapRouter.sol +1 -35
- package/src/interfaces/IZoraFactory.sol +97 -7
- package/src/interfaces/IZoraV4CoinHook.sol +116 -0
- package/src/libs/CoinCommon.sol +15 -0
- package/src/libs/CoinConfigurationVersions.sol +116 -1
- package/src/{utils → libs}/CoinConstants.sol +11 -6
- package/src/libs/CoinDopplerMultiCurve.sol +134 -0
- package/src/libs/CoinDopplerUniV3.sol +19 -171
- package/src/libs/CoinRewards.sol +195 -0
- package/src/libs/CoinRewardsV4.sol +180 -0
- package/src/libs/CoinSetup.sol +40 -20
- package/src/libs/CoinSetupV3.sol +50 -0
- package/src/libs/DopplerMath.sol +156 -0
- package/src/libs/HooksDeployment.sol +84 -0
- package/src/libs/MarketConstants.sol +4 -0
- package/src/libs/PoolStateReader.sol +22 -0
- package/src/libs/UniV3BuySell.sol +231 -0
- package/src/libs/UniV3Errors.sol +11 -0
- package/src/libs/UniV4SwapHelper.sol +65 -0
- package/src/libs/UniV4SwapToCurrency.sol +109 -0
- package/src/libs/V4Liquidity.sol +129 -0
- package/src/types/PoolConfiguration.sol +15 -0
- package/src/utils/DeployedCoinVersionLookup.sol +52 -0
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/Coin.t.sol +94 -101
- package/test/CoinDopplerUniV3.t.sol +35 -184
- package/test/CoinUniV4.t.sol +752 -0
- package/test/DeploymentHooks.t.sol +270 -0
- package/test/Factory.t.sol +84 -50
- package/test/MultiOwnable.t.sol +6 -3
- package/test/Upgrades.t.sol +68 -0
- package/test/mocks/MockERC20.sol +12 -0
- package/test/utils/BaseTest.sol +124 -59
- package/test/utils/DeployedCoinVersionLookup.t.sol +127 -0
- package/test/utils/FeeEstimatorHook.sol +84 -0
- package/test/utils/ProxyShim.sol +17 -0
- package/wagmi.config.ts +10 -9
- package/src/libs/CoinLegacy.sol +0 -48
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import {BaseTest} from "./utils/BaseTest.sol";
|
|
5
|
+
import {BuySupplyWithSwapRouterHook} from "../src/hooks/deployment/BuySupplyWithSwapRouterHook.sol";
|
|
6
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
7
|
+
import {IUniswapV3Pool} from "../src/interfaces/IUniswapV3Pool.sol";
|
|
8
|
+
import {Coin} from "../src/Coin.sol";
|
|
9
|
+
import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
|
|
10
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
11
|
+
import {ICoin} from "../src/interfaces/ICoin.sol";
|
|
12
|
+
import {IHasAfterCoinDeploy} from "../src/hooks/deployment/BaseCoinDeployHook.sol";
|
|
13
|
+
import {IZoraFactory} from "../src/interfaces/IZoraFactory.sol";
|
|
14
|
+
import {ISwapRouter} from "../src/interfaces/ISwapRouter.sol";
|
|
15
|
+
import {CoinConstants} from "../src/libs/CoinConstants.sol";
|
|
16
|
+
|
|
17
|
+
// Create a fake hook that doesn't support the IHasAfterCoinDeploy interface
|
|
18
|
+
contract FakeHookNoInterface {
|
|
19
|
+
function supportsInterface(bytes4) external pure returns (bool) {
|
|
20
|
+
return false; // Always returns false, doesn't support any interface
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
contract HooksTest is BaseTest {
|
|
25
|
+
address constant zora = 0x1111111111166b7FE7bd91427724B487980aFc69;
|
|
26
|
+
|
|
27
|
+
function _generateDefaultPoolConfig(address currency) internal pure returns (bytes memory) {
|
|
28
|
+
return
|
|
29
|
+
_generatePoolConfig(
|
|
30
|
+
CoinConfigurationVersions.DOPPLER_UNI_V3_POOL_VERSION,
|
|
31
|
+
currency,
|
|
32
|
+
DEFAULT_DISCOVERY_TICK_LOWER,
|
|
33
|
+
DEFAULT_DISCOVERY_TICK_UPPER,
|
|
34
|
+
DEFAULT_NUM_DISCOVERY_POSITIONS,
|
|
35
|
+
DEFAULT_DISCOVERY_SUPPLY_SHARE
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setUp() public override {
|
|
40
|
+
super.setUpWithBlockNumber(29585474);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _deployWithHook(address hook, bytes memory hookData, address currency) internal returns (address, bytes memory) {
|
|
44
|
+
bytes memory poolConfig = _generateDefaultPoolConfig(currency);
|
|
45
|
+
return
|
|
46
|
+
factory.deployWithHook(
|
|
47
|
+
users.creator,
|
|
48
|
+
_getDefaultOwners(),
|
|
49
|
+
"https://test.com",
|
|
50
|
+
"Testcoin",
|
|
51
|
+
"TEST",
|
|
52
|
+
poolConfig,
|
|
53
|
+
users.platformReferrer,
|
|
54
|
+
hook,
|
|
55
|
+
hookData
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _encodeAfterCoinDeploy(address buyRecipient, bytes memory swapRouterCall) internal pure returns (bytes memory) {
|
|
60
|
+
return abi.encode(buyRecipient, swapRouterCall);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _encodeExactInputSingle(address buyRecipient, ISwapRouter.ExactInputSingleParams memory params) internal pure returns (bytes memory) {
|
|
64
|
+
return _encodeAfterCoinDeploy(buyRecipient, abi.encodeWithSelector(ISwapRouter.exactInputSingle.selector, params));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _encodeExactInput(address buyRecipient, ISwapRouter.ExactInputParams memory params) internal pure returns (bytes memory) {
|
|
68
|
+
return _encodeAfterCoinDeploy(buyRecipient, abi.encodeWithSelector(ISwapRouter.exactInput.selector, params));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function test_buySupplyWithEthUsingV3Hook_withExactInputSingle(uint256 initialOrderSize) public {
|
|
72
|
+
vm.assume(initialOrderSize > CoinConstants.MIN_ORDER_SIZE);
|
|
73
|
+
vm.assume(initialOrderSize < 1 ether);
|
|
74
|
+
|
|
75
|
+
vm.deal(users.creator, initialOrderSize);
|
|
76
|
+
|
|
77
|
+
BuySupplyWithSwapRouterHook hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter));
|
|
78
|
+
|
|
79
|
+
bytes memory hookData = _encodeExactInputSingle(
|
|
80
|
+
users.creator,
|
|
81
|
+
ISwapRouter.ExactInputSingleParams({
|
|
82
|
+
tokenIn: address(weth),
|
|
83
|
+
tokenOut: zora,
|
|
84
|
+
fee: 3000,
|
|
85
|
+
recipient: address(hook),
|
|
86
|
+
amountIn: initialOrderSize,
|
|
87
|
+
amountOutMinimum: 0,
|
|
88
|
+
sqrtPriceLimitX96: 0
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
vm.prank(users.creator);
|
|
93
|
+
(address coinAddress, bytes memory hookDataOut) = factory.deployWithHook{value: initialOrderSize}(
|
|
94
|
+
users.creator,
|
|
95
|
+
_getDefaultOwners(),
|
|
96
|
+
"https://test.com",
|
|
97
|
+
"Testcoin",
|
|
98
|
+
"TEST",
|
|
99
|
+
_generateDefaultPoolConfig(zora),
|
|
100
|
+
users.platformReferrer,
|
|
101
|
+
address(hook),
|
|
102
|
+
hookData
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
coin = Coin(payable(coinAddress));
|
|
106
|
+
pool = IUniswapV3Pool(coin.poolAddress());
|
|
107
|
+
|
|
108
|
+
(uint256 amountCurrency, uint256 coinsPurchased) = abi.decode(hookDataOut, (uint256, uint256));
|
|
109
|
+
|
|
110
|
+
assertEq(coin.currency(), zora, "currency");
|
|
111
|
+
assertGt(amountCurrency, 0, "amountCurrency > 0");
|
|
112
|
+
assertGt(coinsPurchased, 0, "coinsPurchased > 0");
|
|
113
|
+
assertEq(coin.balanceOf(users.creator), CoinConstants.CREATOR_LAUNCH_REWARD + coinsPurchased, "balanceOf creator");
|
|
114
|
+
assertGt(IERC20(zora).balanceOf(address(pool)), 0, "Pool ZORA balance");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function test_buySupplyWithEthUsingV3Hook_withExactInputMultiHop(uint256 initialOrderSize) public {
|
|
118
|
+
vm.assume(initialOrderSize > CoinConstants.MIN_ORDER_SIZE);
|
|
119
|
+
vm.assume(initialOrderSize < 1 ether);
|
|
120
|
+
|
|
121
|
+
vm.deal(users.creator, initialOrderSize);
|
|
122
|
+
|
|
123
|
+
// lets try weth to usdc to zora
|
|
124
|
+
|
|
125
|
+
BuySupplyWithSwapRouterHook hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter));
|
|
126
|
+
|
|
127
|
+
uint24 poolFee = 3000;
|
|
128
|
+
|
|
129
|
+
bytes memory hookData = _encodeExactInput(
|
|
130
|
+
users.creator,
|
|
131
|
+
ISwapRouter.ExactInputParams({
|
|
132
|
+
path: abi.encodePacked(address(weth), poolFee, USDC_ADDRESS, poolFee, zora),
|
|
133
|
+
recipient: address(hook),
|
|
134
|
+
amountIn: initialOrderSize,
|
|
135
|
+
amountOutMinimum: 0
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
vm.prank(users.creator);
|
|
140
|
+
(address coinAddress, bytes memory hookDataOut) = factory.deployWithHook{value: initialOrderSize}(
|
|
141
|
+
users.creator,
|
|
142
|
+
_getDefaultOwners(),
|
|
143
|
+
"https://test.com",
|
|
144
|
+
"Testcoin",
|
|
145
|
+
"TEST",
|
|
146
|
+
_generateDefaultPoolConfig(zora),
|
|
147
|
+
users.platformReferrer,
|
|
148
|
+
address(hook),
|
|
149
|
+
hookData
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
coin = Coin(payable(coinAddress));
|
|
153
|
+
pool = IUniswapV3Pool(coin.poolAddress());
|
|
154
|
+
|
|
155
|
+
(uint256 amountCurrency, uint256 coinsPurchased) = abi.decode(hookDataOut, (uint256, uint256));
|
|
156
|
+
|
|
157
|
+
assertEq(coin.currency(), zora, "currency");
|
|
158
|
+
assertGt(amountCurrency, 0, "amountCurrency > 0");
|
|
159
|
+
assertGt(coinsPurchased, 0, "coinsPurchased > 0");
|
|
160
|
+
assertEq(coin.balanceOf(users.creator), CoinConstants.CREATOR_LAUNCH_REWARD + coinsPurchased, "balanceOf creator");
|
|
161
|
+
assertGt(IERC20(zora).balanceOf(address(pool)), 0, "Pool ZORA balance");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function test_buySupplyWithEthUsingV3Hook_revertsWhenBadCall() public {
|
|
165
|
+
vm.deal(users.creator, 0.0001 ether);
|
|
166
|
+
|
|
167
|
+
BuySupplyWithSwapRouterHook hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter));
|
|
168
|
+
|
|
169
|
+
uint24 poolFee = 3000;
|
|
170
|
+
|
|
171
|
+
// exact output single is not supported
|
|
172
|
+
bytes memory hookData = _encodeAfterCoinDeploy(
|
|
173
|
+
users.creator,
|
|
174
|
+
abi.encodeWithSelector(
|
|
175
|
+
ISwapRouter.exactOutputSingle.selector,
|
|
176
|
+
ISwapRouter.ExactOutputParams({
|
|
177
|
+
path: abi.encodePacked(address(weth), poolFee, USDC_ADDRESS, poolFee, zora),
|
|
178
|
+
recipient: address(hook),
|
|
179
|
+
amountOut: 0.0001 ether,
|
|
180
|
+
amountInMaximum: 0
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
vm.prank(users.creator);
|
|
186
|
+
vm.expectRevert(BuySupplyWithSwapRouterHook.InvalidSwapRouterCall.selector);
|
|
187
|
+
factory.deployWithHook{value: 0.0001 ether}(
|
|
188
|
+
users.creator,
|
|
189
|
+
_getDefaultOwners(),
|
|
190
|
+
"https://test.com",
|
|
191
|
+
"Testcoin",
|
|
192
|
+
"TEST",
|
|
193
|
+
_generateDefaultPoolConfig(zora),
|
|
194
|
+
users.platformReferrer,
|
|
195
|
+
address(hook),
|
|
196
|
+
hookData
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function test_buySupplyWithEthUsingV3Hook_revertsWhenHookNotRecipient() public {
|
|
201
|
+
uint256 initialOrderSize = 0.0001 ether;
|
|
202
|
+
vm.deal(users.creator, initialOrderSize);
|
|
203
|
+
|
|
204
|
+
BuySupplyWithSwapRouterHook hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter));
|
|
205
|
+
|
|
206
|
+
bytes memory hookData = _encodeExactInputSingle(
|
|
207
|
+
users.creator,
|
|
208
|
+
ISwapRouter.ExactInputSingleParams({
|
|
209
|
+
tokenIn: address(weth),
|
|
210
|
+
tokenOut: zora,
|
|
211
|
+
fee: 3000,
|
|
212
|
+
recipient: address(users.creator),
|
|
213
|
+
amountIn: initialOrderSize,
|
|
214
|
+
amountOutMinimum: 0,
|
|
215
|
+
sqrtPriceLimitX96: 0
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
vm.prank(users.creator);
|
|
220
|
+
vm.expectRevert(BuySupplyWithSwapRouterHook.Erc20NotReceived.selector);
|
|
221
|
+
factory.deployWithHook{value: initialOrderSize}(
|
|
222
|
+
users.creator,
|
|
223
|
+
_getDefaultOwners(),
|
|
224
|
+
"https://test.com",
|
|
225
|
+
"Testcoin",
|
|
226
|
+
"TEST",
|
|
227
|
+
_generateDefaultPoolConfig(zora),
|
|
228
|
+
users.platformReferrer,
|
|
229
|
+
address(hook),
|
|
230
|
+
hookData
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function test_deployWithHook_revertsWhenEthAndNoHook() public {
|
|
235
|
+
uint256 initialOrderSize = 0.0001 ether;
|
|
236
|
+
vm.deal(users.creator, initialOrderSize);
|
|
237
|
+
|
|
238
|
+
vm.prank(users.creator);
|
|
239
|
+
vm.expectRevert(IZoraFactory.EthTransferInvalid.selector);
|
|
240
|
+
factory.deployWithHook{value: initialOrderSize}(
|
|
241
|
+
users.creator,
|
|
242
|
+
_getDefaultOwners(),
|
|
243
|
+
"https://test.com",
|
|
244
|
+
"Testcoin",
|
|
245
|
+
"TEST",
|
|
246
|
+
_generateDefaultPoolConfig(zora),
|
|
247
|
+
users.platformReferrer,
|
|
248
|
+
address(0),
|
|
249
|
+
""
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function test_invalidHookReverts() public {
|
|
254
|
+
// Deploy a fake hook that doesn't support the IHasAfterCoinDeploy interface
|
|
255
|
+
FakeHookNoInterface fakeHook = new FakeHookNoInterface();
|
|
256
|
+
|
|
257
|
+
bytes memory hookData = "";
|
|
258
|
+
|
|
259
|
+
// Expect the transaction to revert with InvalidHook error
|
|
260
|
+
vm.expectRevert(IZoraFactory.InvalidHook.selector);
|
|
261
|
+
vm.prank(users.creator);
|
|
262
|
+
_deployWithHook(address(fakeHook), hookData, zora);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function test_noHookWorksAsNormal() public {
|
|
266
|
+
// Expect the transaction to revert with InvalidHook error
|
|
267
|
+
vm.prank(users.creator);
|
|
268
|
+
_deployWithHook(address(0), bytes(""), zora);
|
|
269
|
+
}
|
|
270
|
+
}
|
package/test/Factory.t.sol
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
pragma solidity ^0.8.13;
|
|
3
3
|
|
|
4
4
|
import "./utils/BaseTest.sol";
|
|
5
|
+
import {CoinConstants} from "../src/libs/CoinConstants.sol";
|
|
5
6
|
|
|
6
7
|
contract FactoryTest is BaseTest {
|
|
7
8
|
function setUp() public override {
|
|
@@ -9,8 +10,9 @@ contract FactoryTest is BaseTest {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
function test_constructor() public view {
|
|
12
|
-
assertEq(factory.coinImpl(), address(
|
|
13
|
-
assertEq(factory.owner(), users.factoryOwner);
|
|
13
|
+
assertEq(ZoraFactoryImpl(address(factory)).coinImpl(), address(coinV3Impl));
|
|
14
|
+
assertEq(ZoraFactoryImpl(address(factory)).owner(), users.factoryOwner);
|
|
15
|
+
assertEq(ZoraFactoryImpl(address(factory)).coinV4Impl(), address(coinV4Impl));
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function test_deploy_no_eth() public {
|
|
@@ -23,9 +25,8 @@ contract FactoryTest is BaseTest {
|
|
|
23
25
|
"https://test2.com",
|
|
24
26
|
"Test2 Token",
|
|
25
27
|
"TEST2",
|
|
28
|
+
_generatePoolConfig(address(weth)),
|
|
26
29
|
users.platformReferrer,
|
|
27
|
-
address(weth),
|
|
28
|
-
MarketConstants.LP_TICK_LOWER_WETH,
|
|
29
30
|
0
|
|
30
31
|
);
|
|
31
32
|
coin = Coin(payable(coinAddress));
|
|
@@ -58,7 +59,7 @@ contract FactoryTest is BaseTest {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function test_deploy_with_eth(uint256 initialOrderSize) public {
|
|
61
|
-
vm.assume(initialOrderSize > MIN_ORDER_SIZE);
|
|
62
|
+
vm.assume(initialOrderSize > CoinConstants.MIN_ORDER_SIZE);
|
|
62
63
|
vm.assume(initialOrderSize < 10 ether);
|
|
63
64
|
|
|
64
65
|
address[] memory owners = new address[](1);
|
|
@@ -72,9 +73,8 @@ contract FactoryTest is BaseTest {
|
|
|
72
73
|
"https://test2.com",
|
|
73
74
|
"Test2 Token",
|
|
74
75
|
"TEST2",
|
|
76
|
+
_generatePoolConfig(address(weth)),
|
|
75
77
|
users.platformReferrer,
|
|
76
|
-
address(weth),
|
|
77
|
-
MarketConstants.LP_TICK_LOWER_WETH,
|
|
78
78
|
initialOrderSize
|
|
79
79
|
);
|
|
80
80
|
coin = Coin(payable(coinAddress));
|
|
@@ -97,7 +97,7 @@ contract FactoryTest is BaseTest {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function test_deploy_with_weth(uint256 initialOrderSize) public {
|
|
100
|
-
vm.assume(initialOrderSize > MIN_ORDER_SIZE);
|
|
100
|
+
vm.assume(initialOrderSize > CoinConstants.MIN_ORDER_SIZE);
|
|
101
101
|
vm.assume(initialOrderSize < 10 ether);
|
|
102
102
|
|
|
103
103
|
address[] memory owners = new address[](1);
|
|
@@ -118,9 +118,8 @@ contract FactoryTest is BaseTest {
|
|
|
118
118
|
"https://test2.com",
|
|
119
119
|
"Test2 Token",
|
|
120
120
|
"TEST2",
|
|
121
|
+
_generatePoolConfig(address(weth)),
|
|
121
122
|
users.platformReferrer,
|
|
122
|
-
address(weth),
|
|
123
|
-
MarketConstants.LP_TICK_LOWER_WETH,
|
|
124
123
|
initialOrderSize
|
|
125
124
|
);
|
|
126
125
|
}
|
|
@@ -138,9 +137,8 @@ contract FactoryTest is BaseTest {
|
|
|
138
137
|
"https://test2.com",
|
|
139
138
|
"Test2 Token",
|
|
140
139
|
"TEST2",
|
|
140
|
+
_generatePoolConfig(address(weth)),
|
|
141
141
|
users.platformReferrer,
|
|
142
|
-
address(weth),
|
|
143
|
-
MarketConstants.LP_TICK_LOWER_WETH,
|
|
144
142
|
orderSize
|
|
145
143
|
);
|
|
146
144
|
coin = Coin(payable(coinAddress));
|
|
@@ -159,9 +157,8 @@ contract FactoryTest is BaseTest {
|
|
|
159
157
|
"https://testcoinusdcpair.com",
|
|
160
158
|
"Testcoinusdcpair",
|
|
161
159
|
"TESTCOINUSDCPAIR",
|
|
160
|
+
_generatePoolConfig(USDC_ADDRESS),
|
|
162
161
|
users.platformReferrer,
|
|
163
|
-
USDC_ADDRESS,
|
|
164
|
-
USDC_TICK_LOWER,
|
|
165
162
|
0
|
|
166
163
|
);
|
|
167
164
|
coin = Coin(payable(coinAddress));
|
|
@@ -192,9 +189,8 @@ contract FactoryTest is BaseTest {
|
|
|
192
189
|
"https://testcoinusdcpair.com",
|
|
193
190
|
"Testcoinusdcpair",
|
|
194
191
|
"TESTCOINUSDCPAIR",
|
|
192
|
+
_generatePoolConfig(USDC_ADDRESS),
|
|
195
193
|
users.platformReferrer,
|
|
196
|
-
USDC_ADDRESS,
|
|
197
|
-
USDC_TICK_LOWER,
|
|
198
194
|
orderSize
|
|
199
195
|
);
|
|
200
196
|
coin = Coin(payable(coinAddress));
|
|
@@ -203,7 +199,7 @@ contract FactoryTest is BaseTest {
|
|
|
203
199
|
vm.label(address(pool), "POOL");
|
|
204
200
|
|
|
205
201
|
assertEq(coin.currency(), USDC_ADDRESS, "currency");
|
|
206
|
-
assertEq(coin.balanceOf(users.creator), CREATOR_LAUNCH_REWARD + coinsPurchased);
|
|
202
|
+
assertEq(coin.balanceOf(users.creator), CoinConstants.CREATOR_LAUNCH_REWARD + coinsPurchased);
|
|
207
203
|
}
|
|
208
204
|
|
|
209
205
|
function test_deploy_with_usdc_revert_payout_recipient_zero() public {
|
|
@@ -217,9 +213,8 @@ contract FactoryTest is BaseTest {
|
|
|
217
213
|
"https://testcoinusdcpair.com",
|
|
218
214
|
"Testcoinusdcpair",
|
|
219
215
|
"TESTCOINUSDCPAIR",
|
|
216
|
+
_generatePoolConfig(USDC_ADDRESS),
|
|
220
217
|
users.platformReferrer,
|
|
221
|
-
USDC_ADDRESS,
|
|
222
|
-
USDC_TICK_LOWER,
|
|
223
218
|
0
|
|
224
219
|
);
|
|
225
220
|
}
|
|
@@ -234,9 +229,8 @@ contract FactoryTest is BaseTest {
|
|
|
234
229
|
"https://testcoinusdcpair.com",
|
|
235
230
|
"Testcoinusdcpair",
|
|
236
231
|
"TESTCOINUSDCPAIR",
|
|
232
|
+
_generatePoolConfig(USDC_ADDRESS),
|
|
237
233
|
users.platformReferrer,
|
|
238
|
-
USDC_ADDRESS,
|
|
239
|
-
USDC_TICK_LOWER,
|
|
240
234
|
0
|
|
241
235
|
);
|
|
242
236
|
}
|
|
@@ -251,9 +245,8 @@ contract FactoryTest is BaseTest {
|
|
|
251
245
|
"https://testcoinusdcpair.com",
|
|
252
246
|
"Testcoinusdcpair",
|
|
253
247
|
"TESTCOINUSDCPAIR",
|
|
248
|
+
_generatePoolConfig(USDC_ADDRESS),
|
|
254
249
|
address(0),
|
|
255
|
-
USDC_ADDRESS,
|
|
256
|
-
USDC_TICK_LOWER,
|
|
257
250
|
0
|
|
258
251
|
);
|
|
259
252
|
|
|
@@ -262,24 +255,6 @@ contract FactoryTest is BaseTest {
|
|
|
262
255
|
assertEq(coin.platformReferrer(), coin.protocolRewardRecipient(), "platformReferrer");
|
|
263
256
|
}
|
|
264
257
|
|
|
265
|
-
function test_revert_deploy_with_invalid_currency_tick() public {
|
|
266
|
-
address[] memory owners = new address[](1);
|
|
267
|
-
owners[0] = users.creator;
|
|
268
|
-
|
|
269
|
-
vm.expectRevert(abi.encodeWithSelector(ICoin.InvalidWethLowerTick.selector));
|
|
270
|
-
factory.deploy(
|
|
271
|
-
users.creator,
|
|
272
|
-
owners,
|
|
273
|
-
"https://testcoin.com",
|
|
274
|
-
"Testcoin",
|
|
275
|
-
"TESTCOIN",
|
|
276
|
-
users.platformReferrer,
|
|
277
|
-
address(0),
|
|
278
|
-
MarketConstants.LP_TICK_LOWER_WETH + 1,
|
|
279
|
-
0
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
258
|
function test_deploy_with_usdc_revert_invalid_eth_transfer() public {
|
|
284
259
|
address[] memory owners = new address[](1);
|
|
285
260
|
owners[0] = users.creator;
|
|
@@ -297,9 +272,8 @@ contract FactoryTest is BaseTest {
|
|
|
297
272
|
"https://testcoinusdcpair.com",
|
|
298
273
|
"Testcoinusdcpair",
|
|
299
274
|
"TESTCOINUSDCPAIR",
|
|
275
|
+
_generatePoolConfig(USDC_ADDRESS),
|
|
300
276
|
users.platformReferrer,
|
|
301
|
-
USDC_ADDRESS,
|
|
302
|
-
USDC_TICK_LOWER,
|
|
303
277
|
0
|
|
304
278
|
);
|
|
305
279
|
}
|
|
@@ -314,9 +288,8 @@ contract FactoryTest is BaseTest {
|
|
|
314
288
|
"https://test.com",
|
|
315
289
|
"Test Token",
|
|
316
290
|
"TEST",
|
|
291
|
+
_generatePoolConfig(address(weth)),
|
|
317
292
|
users.platformReferrer,
|
|
318
|
-
address(weth),
|
|
319
|
-
MarketConstants.LP_TICK_LOWER_WETH,
|
|
320
293
|
0
|
|
321
294
|
);
|
|
322
295
|
coin = Coin(payable(coinAddress));
|
|
@@ -325,10 +298,10 @@ contract FactoryTest is BaseTest {
|
|
|
325
298
|
}
|
|
326
299
|
|
|
327
300
|
function test_upgrade() public {
|
|
328
|
-
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(
|
|
301
|
+
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(coinV3Impl), address(coinV4Impl));
|
|
329
302
|
|
|
330
303
|
vm.prank(users.factoryOwner);
|
|
331
|
-
factory.upgradeToAndCall(address(newImpl), "");
|
|
304
|
+
ZoraFactoryImpl(address(factory)).upgradeToAndCall(address(newImpl), "");
|
|
332
305
|
|
|
333
306
|
assertEq(factory.implementation(), address(newImpl), "implementation");
|
|
334
307
|
}
|
|
@@ -342,14 +315,75 @@ contract FactoryTest is BaseTest {
|
|
|
342
315
|
|
|
343
316
|
vm.prank(users.factoryOwner);
|
|
344
317
|
vm.expectRevert(abi.encodeWithSelector(ERC1967Utils.ERC1967InvalidImplementation.selector, address(newImpl)));
|
|
345
|
-
factory.upgradeToAndCall(address(newImpl), "");
|
|
318
|
+
ZoraFactoryImpl(address(factory)).upgradeToAndCall(address(newImpl), "");
|
|
346
319
|
}
|
|
347
320
|
|
|
348
321
|
function test_revert_invalid_owner() public {
|
|
349
|
-
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(
|
|
322
|
+
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(coinV3Impl), address(coinV4Impl));
|
|
350
323
|
|
|
351
324
|
vm.prank(users.creator);
|
|
352
325
|
vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.creator));
|
|
353
|
-
factory.upgradeToAndCall(address(newImpl), "");
|
|
326
|
+
ZoraFactoryImpl(address(factory)).upgradeToAndCall(address(newImpl), "");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function test_coinAddress_canBePredicted(
|
|
330
|
+
bool msgSenderChanged,
|
|
331
|
+
bool saltChanged,
|
|
332
|
+
bool poolConfigChanged,
|
|
333
|
+
bool platformReferrerChanged,
|
|
334
|
+
bool nameChanged,
|
|
335
|
+
bool symbolChanged
|
|
336
|
+
) public {
|
|
337
|
+
address[] memory owners = new address[](1);
|
|
338
|
+
owners[0] = users.creator;
|
|
339
|
+
|
|
340
|
+
address payoutRecipient = users.creator;
|
|
341
|
+
|
|
342
|
+
bytes32 salt = keccak256(abi.encode(bytes("randomSalt")));
|
|
343
|
+
|
|
344
|
+
address msgSender = makeAddr("msgSender");
|
|
345
|
+
|
|
346
|
+
string memory uri = "https://test.com";
|
|
347
|
+
string memory name = "Testcoin";
|
|
348
|
+
string memory symbol = "TEST";
|
|
349
|
+
|
|
350
|
+
address platformReferrer = users.platformReferrer;
|
|
351
|
+
|
|
352
|
+
bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(address(weth));
|
|
353
|
+
bytes memory poolConfigForGettingAddress = poolConfigChanged ? CoinConfigurationVersions.defaultDopplerUniV3(address(weth)) : poolConfig;
|
|
354
|
+
|
|
355
|
+
address expectedCoinAddress = factory.coinAddress(msgSender, name, symbol, poolConfigForGettingAddress, platformReferrer, salt);
|
|
356
|
+
|
|
357
|
+
if (msgSenderChanged) {
|
|
358
|
+
msgSender = makeAddr("msgSender2");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (saltChanged) {
|
|
362
|
+
salt = keccak256(abi.encode(bytes("randomSalt2")));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (platformReferrerChanged) {
|
|
366
|
+
platformReferrer = makeAddr("platformReferrer2");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (nameChanged) {
|
|
370
|
+
name = "Testcoin2";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (symbolChanged) {
|
|
374
|
+
symbol = "TEST2";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// now deploy the coin
|
|
378
|
+
vm.prank(msgSender);
|
|
379
|
+
(address coinAddress, ) = factory.deploy(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer, address(0), bytes(""), salt);
|
|
380
|
+
|
|
381
|
+
bool addressShouldMismatch = msgSenderChanged || saltChanged || poolConfigChanged || platformReferrerChanged || nameChanged || symbolChanged;
|
|
382
|
+
|
|
383
|
+
if (addressShouldMismatch) {
|
|
384
|
+
assertNotEq(coinAddress, expectedCoinAddress, "coinAddress should mismatch");
|
|
385
|
+
} else {
|
|
386
|
+
assertEq(coinAddress, expectedCoinAddress, "coinAddress should match");
|
|
387
|
+
}
|
|
354
388
|
}
|
|
355
389
|
}
|
package/test/MultiOwnable.t.sol
CHANGED
|
@@ -135,22 +135,25 @@ 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(weth));
|
|
138
139
|
vm.expectRevert(MultiOwnable.OneOwnerRequired.selector);
|
|
139
|
-
factory.deploy(users.creator, emptyOwners, "https://test.com", "Test Token", "TEST", users.platformReferrer,
|
|
140
|
+
factory.deploy(users.creator, emptyOwners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
function test_revert_init_with_zero_address() public {
|
|
143
144
|
address[] memory owners = new address[](1);
|
|
144
145
|
owners[0] = address(0);
|
|
146
|
+
bytes memory poolConfig_ = _generatePoolConfig(address(weth));
|
|
145
147
|
vm.expectRevert(MultiOwnable.OwnerCannotBeAddressZero.selector);
|
|
146
|
-
factory.deploy(users.creator, owners, "https://test.com", "Test Token", "TEST", users.platformReferrer,
|
|
148
|
+
factory.deploy(users.creator, owners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
function test_revert_init_with_duplicate_owner() public {
|
|
150
152
|
address[] memory owners = new address[](2);
|
|
151
153
|
owners[0] = users.creator;
|
|
152
154
|
owners[1] = users.creator;
|
|
155
|
+
bytes memory poolConfig_ = _generatePoolConfig(address(weth));
|
|
153
156
|
vm.expectRevert(MultiOwnable.AlreadyOwner.selector);
|
|
154
|
-
factory.deploy(users.creator, owners, "https://test.com", "Test Token", "TEST", users.platformReferrer,
|
|
157
|
+
factory.deploy(users.creator, owners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
|
|
155
158
|
}
|
|
156
159
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
import {Test} from "forge-std/Test.sol";
|
|
4
|
+
import {IZoraFactory} from "../src/interfaces/IZoraFactory.sol";
|
|
5
|
+
import {ZoraFactoryImpl} from "../src/ZoraFactoryImpl.sol";
|
|
6
|
+
import {BaseTest} from "./utils/BaseTest.sol";
|
|
7
|
+
|
|
8
|
+
contract BadImpl {
|
|
9
|
+
function contractName() public pure returns (string memory) {
|
|
10
|
+
return "BadImpl";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
contract UpgradesTest is BaseTest {
|
|
15
|
+
ZoraFactoryImpl public factoryProxy;
|
|
16
|
+
|
|
17
|
+
function test_canUpgradeFromVersionWithoutContractName() public {
|
|
18
|
+
// this test that we can upgrade from the current version, which doesn't have a contract name
|
|
19
|
+
vm.createSelectFork("base", 29675508);
|
|
20
|
+
|
|
21
|
+
factoryProxy = ZoraFactoryImpl(0x777777751622c0d3258f214F9DF38E35BF45baF3);
|
|
22
|
+
|
|
23
|
+
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(factoryProxy.coinImpl(), address(coinV4Impl));
|
|
24
|
+
|
|
25
|
+
vm.prank(factoryProxy.owner());
|
|
26
|
+
factoryProxy.upgradeToAndCall(address(newImpl), "");
|
|
27
|
+
|
|
28
|
+
assertEq(factoryProxy.implementation(), address(newImpl));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function test_cannotUpgradeToMismatchedContractName() public {
|
|
32
|
+
// this test that we cannot upgrade to a contract with a mismatched contract name
|
|
33
|
+
// once we have upgraded to the version that checks the contract name when upgrading
|
|
34
|
+
vm.createSelectFork("base", 29675508);
|
|
35
|
+
|
|
36
|
+
factoryProxy = ZoraFactoryImpl(0x777777751622c0d3258f214F9DF38E35BF45baF3);
|
|
37
|
+
|
|
38
|
+
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(factoryProxy.coinImpl(), address(coinV4Impl));
|
|
39
|
+
|
|
40
|
+
vm.prank(factoryProxy.owner());
|
|
41
|
+
factoryProxy.upgradeToAndCall(address(newImpl), "");
|
|
42
|
+
|
|
43
|
+
BadImpl badImpl = new BadImpl();
|
|
44
|
+
|
|
45
|
+
vm.prank(factoryProxy.owner());
|
|
46
|
+
vm.expectRevert(abi.encodeWithSelector(IZoraFactory.UpgradeToMismatchedContractName.selector, "ZoraCoinFactory", "BadImpl"));
|
|
47
|
+
factoryProxy.upgradeToAndCall(address(badImpl), "");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function test_canUpgradeToSameContractName() public {
|
|
51
|
+
// this test that we can upgrade to the same contract name, when we have already upgraded to a version that has a contract name
|
|
52
|
+
vm.createSelectFork("base", 29675508);
|
|
53
|
+
|
|
54
|
+
factoryProxy = ZoraFactoryImpl(0x777777751622c0d3258f214F9DF38E35BF45baF3);
|
|
55
|
+
|
|
56
|
+
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(factoryProxy.coinImpl(), address(coinV4Impl));
|
|
57
|
+
|
|
58
|
+
vm.prank(factoryProxy.owner());
|
|
59
|
+
factoryProxy.upgradeToAndCall(address(newImpl), "");
|
|
60
|
+
|
|
61
|
+
ZoraFactoryImpl newImpl2 = new ZoraFactoryImpl(factoryProxy.coinImpl(), factoryProxy.coinV4Impl());
|
|
62
|
+
|
|
63
|
+
vm.prank(factoryProxy.owner());
|
|
64
|
+
factoryProxy.upgradeToAndCall(address(newImpl2), "");
|
|
65
|
+
|
|
66
|
+
assertEq(factoryProxy.implementation(), address(newImpl2));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
5
|
+
|
|
6
|
+
contract MockERC20 is ERC20 {
|
|
7
|
+
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
|
|
8
|
+
|
|
9
|
+
function mint(address to, uint256 amount) external {
|
|
10
|
+
_mint(to, amount);
|
|
11
|
+
}
|
|
12
|
+
}
|