Skip to content

Commit 7df9491

Browse files
authored
Enable buying initial supply when deploying creator coin, and refactored internals of the factory to share more code (#1417)
* Enabling buying initial supply when deploying a creator coin. * Refactored internals of zora factory iml to share a bunch of code
1 parent ca8aad7 commit 7df9491

File tree

4 files changed

+197
-84
lines changed

4 files changed

+197
-84
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@zoralabs/coins": minor
3+
---
4+
5+
Enable buying initial supply when deploying creator coin and refactor factory internals
6+
7+
- Add new `deployCreatorCoin` overload with `postDeployHook` parameter that supports ETH transfers
8+
- Refactored internal factory implementation to share more code between deployment methods
9+
- Enables buying initial supply during creator coin deployment via post-deploy hooks

packages/coins/src/ZoraFactoryImpl.sol

Lines changed: 115 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,7 @@ contract ZoraFactoryImpl is
6666
zoraHookRegistry = zoraHookRegistry_;
6767
}
6868

69-
/// @notice Creates a new creator coin contract
70-
/// @param payoutRecipient The recipient of creator reward payouts; this can be updated by an owner
71-
/// @param owners The list of addresses that will be able to manage the coin's payout address and metadata uri
72-
/// @param uri The coin metadata uri
73-
/// @param name The name of the coin
74-
/// @param symbol The symbol of the coin
75-
/// @param poolConfig The config parameters for the coin's pool
76-
/// @param platformReferrer The address of the platform referrer
77-
/// @param coinSalt The salt used to deploy the coin
69+
/// @inheritdoc IZoraFactory
7870
function deployCreatorCoin(
7971
address payoutRecipient,
8072
address[] memory owners,
@@ -86,39 +78,24 @@ contract ZoraFactoryImpl is
8678
bytes32 coinSalt
8779
) public nonReentrant returns (address) {
8880
bytes32 salt = _buildSalt(msg.sender, name, symbol, poolConfig, platformReferrer, coinSalt);
81+
return address(_createAndInitializeCreatorCoin(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer, salt));
82+
}
8983

90-
uint8 version = CoinConfigurationVersions.getVersion(poolConfig);
91-
92-
require(version == CoinConfigurationVersions.DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION, InvalidConfig());
93-
94-
address creatorCoin = Clones.cloneDeterministic(creatorCoinImpl, salt);
95-
96-
_setVersionForDeployedCoin(address(creatorCoin), version);
97-
98-
(, address currency, uint160 sqrtPriceX96, bool isCoinToken0, PoolConfiguration memory poolConfiguration) = CoinSetup.generatePoolConfig(
99-
address(creatorCoin),
100-
poolConfig
101-
);
102-
103-
PoolKey memory poolKey = CoinSetup.buildPoolKey(address(creatorCoin), currency, isCoinToken0, IHooks(hook));
104-
105-
ICreatorCoin(creatorCoin).initialize(payoutRecipient, owners, uri, name, symbol, platformReferrer, currency, poolKey, sqrtPriceX96, poolConfiguration);
106-
107-
emit CreatorCoinCreated(
108-
msg.sender,
109-
payoutRecipient,
110-
platformReferrer,
111-
currency,
112-
uri,
113-
name,
114-
symbol,
115-
address(creatorCoin),
116-
poolKey,
117-
CoinCommon.hashPoolKey(poolKey),
118-
IVersionedContract(address(creatorCoin)).contractVersion()
119-
);
120-
121-
return creatorCoin;
84+
/// @inheritdoc IZoraFactory
85+
function deployCreatorCoin(
86+
address payoutRecipient,
87+
address[] memory owners,
88+
string memory uri,
89+
string memory name,
90+
string memory symbol,
91+
bytes memory poolConfig,
92+
address platformReferrer,
93+
address postDeployHook,
94+
bytes calldata postDeployHookData,
95+
bytes32 coinSalt
96+
) external payable nonReentrant returns (address coin, bytes memory postDeployHookDataOut) {
97+
bytes32 salt = _buildSalt(msg.sender, name, symbol, poolConfig, platformReferrer, coinSalt);
98+
return _deployCreatorCoinWithHook(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer, postDeployHook, postDeployHookData, salt);
12299
}
123100

124101
/// @inheritdoc IZoraFactory
@@ -151,6 +128,18 @@ contract ZoraFactoryImpl is
151128
return Clones.predictDeterministicAddress(getCoinImpl(CoinConfigurationVersions.getVersion(poolConfig)), salt, address(this));
152129
}
153130

131+
function _executePostDeployHook(address coin, address deployHook, bytes calldata hookData) internal returns (bytes memory hookDataOut) {
132+
if (deployHook != address(0)) {
133+
if (!IERC165(deployHook).supportsInterface(type(IHasAfterCoinDeploy).interfaceId)) {
134+
revert InvalidHook();
135+
}
136+
hookDataOut = IHasAfterCoinDeploy(deployHook).afterCoinDeploy{value: msg.value}(msg.sender, ICoin(coin), hookData);
137+
} else if (msg.value > 0) {
138+
// cannot send eth without a hook
139+
revert EthTransferInvalid();
140+
}
141+
}
142+
154143
/// @dev Internal function to deploy a coin with a hook
155144
function _deployWithHook(
156145
address payoutRecipient,
@@ -165,16 +154,24 @@ contract ZoraFactoryImpl is
165154
bytes32 salt
166155
) internal returns (address coin, bytes memory hookDataOut) {
167156
coin = address(_createAndInitializeCoin(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer, salt));
157+
hookDataOut = _executePostDeployHook(coin, deployHook, hookData);
158+
}
168159

169-
if (deployHook != address(0)) {
170-
if (!IERC165(deployHook).supportsInterface(type(IHasAfterCoinDeploy).interfaceId)) {
171-
revert InvalidHook();
172-
}
173-
hookDataOut = IHasAfterCoinDeploy(deployHook).afterCoinDeploy{value: msg.value}(msg.sender, ICoin(coin), hookData);
174-
} else if (msg.value > 0) {
175-
// cannot send eth without a hook
176-
revert EthTransferInvalid();
177-
}
160+
/// @dev Internal function to deploy a creator coin with a hook
161+
function _deployCreatorCoinWithHook(
162+
address payoutRecipient,
163+
address[] memory owners,
164+
string memory uri,
165+
string memory name,
166+
string memory symbol,
167+
bytes memory poolConfig,
168+
address platformReferrer,
169+
address deployHook,
170+
bytes calldata hookData,
171+
bytes32 salt
172+
) internal returns (address coin, bytes memory hookDataOut) {
173+
coin = address(_createAndInitializeCreatorCoin(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer, salt));
174+
hookDataOut = _executePostDeployHook(coin, deployHook, hookData);
178175
}
179176

180177
/**
@@ -251,41 +248,69 @@ contract ZoraFactoryImpl is
251248
revert ICoin.InvalidPoolVersion();
252249
}
253250

254-
function _createCoin(uint8 version, bytes32 salt) internal returns (address payable) {
255-
return payable(Clones.cloneDeterministic(getCoinImpl(version), salt));
251+
function _createCoinWithPoolConfig(
252+
address _implementation,
253+
bytes memory poolConfig,
254+
bytes32 coinSalt,
255+
address payoutRecipient,
256+
address[] memory owners,
257+
string memory uri,
258+
string memory name,
259+
string memory symbol,
260+
address platformReferrer
261+
) internal returns (address coin, uint8 version, PoolKey memory poolKey, address currency) {
262+
version = CoinConfigurationVersions.getVersion(poolConfig);
263+
coin = Clones.cloneDeterministic(_implementation, coinSalt);
264+
_setVersionForDeployedCoin(coin, version);
265+
266+
uint160 sqrtPriceX96;
267+
bool isCoinToken0;
268+
PoolConfiguration memory poolConfiguration;
269+
(, currency, sqrtPriceX96, isCoinToken0, poolConfiguration) = CoinSetup.generatePoolConfig(coin, poolConfig);
270+
271+
poolKey = CoinSetup.buildPoolKey(coin, currency, isCoinToken0, IHooks(hook));
272+
ICoin(coin).initialize(payoutRecipient, owners, uri, name, symbol, platformReferrer, currency, poolKey, sqrtPriceX96, poolConfiguration);
256273
}
257274

258-
function _setupV4Coin(
259-
ICoin coin,
260-
address currency,
261-
bool isCoinToken0,
262-
uint160 sqrtPriceX96,
263-
PoolConfiguration memory poolConfiguration,
275+
function _createAndInitializeCreatorCoin(
264276
address payoutRecipient,
265277
address[] memory owners,
266278
string memory uri,
267279
string memory name,
268280
string memory symbol,
269-
address platformReferrer
270-
) internal {
271-
PoolKey memory poolKey = CoinSetup.buildPoolKey(address(coin), currency, isCoinToken0, IHooks(hook));
281+
bytes memory poolConfig,
282+
address platformReferrer,
283+
bytes32 coinSalt
284+
) internal returns (ICreatorCoin) {
285+
(address creatorCoin, uint8 version, PoolKey memory poolKey, address currency) = _createCoinWithPoolConfig(
286+
creatorCoinImpl,
287+
poolConfig,
288+
coinSalt,
289+
payoutRecipient,
290+
owners,
291+
uri,
292+
name,
293+
symbol,
294+
platformReferrer
295+
);
272296

273-
// Initialize coin with pre-configured pool
274-
coin.initialize(payoutRecipient, owners, uri, name, symbol, platformReferrer, currency, poolKey, sqrtPriceX96, poolConfiguration);
297+
require(version == CoinConfigurationVersions.DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION, InvalidConfig());
275298

276-
emit CoinCreatedV4(
299+
emit CreatorCoinCreated(
277300
msg.sender,
278301
payoutRecipient,
279302
platformReferrer,
280303
currency,
281304
uri,
282305
name,
283306
symbol,
284-
address(coin),
307+
creatorCoin,
285308
poolKey,
286309
CoinCommon.hashPoolKey(poolKey),
287-
IVersionedContract(address(coin)).contractVersion()
310+
IVersionedContract(creatorCoin).contractVersion()
288311
);
312+
313+
return ICreatorCoin(creatorCoin);
289314
}
290315

291316
function _createAndInitializeCoin(
@@ -298,26 +323,34 @@ contract ZoraFactoryImpl is
298323
address platformReferrer,
299324
bytes32 coinSalt
300325
) internal returns (ICoin) {
301-
uint8 version = CoinConfigurationVersions.getVersion(poolConfig);
302-
303-
address payable coin = _createCoin(version, coinSalt);
326+
(address coin, uint8 version, PoolKey memory poolKey, address currency) = _createCoinWithPoolConfig(
327+
coinV4Impl,
328+
poolConfig,
329+
coinSalt,
330+
payoutRecipient,
331+
owners,
332+
uri,
333+
name,
334+
symbol,
335+
platformReferrer
336+
);
304337

305-
_setVersionForDeployedCoin(address(coin), version);
338+
require(version == CoinConfigurationVersions.DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION, ICoin.InvalidPoolVersion());
306339

307-
(, address currency, uint160 sqrtPriceX96, bool isCoinToken0, PoolConfiguration memory poolConfiguration) = CoinSetup.generatePoolConfig(
308-
address(coin),
309-
poolConfig
340+
emit CoinCreatedV4(
341+
msg.sender,
342+
payoutRecipient,
343+
platformReferrer,
344+
currency,
345+
uri,
346+
name,
347+
symbol,
348+
coin,
349+
poolKey,
350+
CoinCommon.hashPoolKey(poolKey),
351+
IVersionedContract(coin).contractVersion()
310352
);
311353

312-
if (CoinConfigurationVersions.isV3(version)) {
313-
// V3 is no longer supported
314-
revert ICoin.InvalidPoolVersion();
315-
} else if (CoinConfigurationVersions.isV4(version)) {
316-
_setupV4Coin(ICoin(coin), currency, isCoinToken0, sqrtPriceX96, poolConfiguration, payoutRecipient, owners, uri, name, symbol, platformReferrer);
317-
} else {
318-
revert ICoin.InvalidPoolVersion();
319-
}
320-
321354
return ICoin(coin);
322355
}
323356

packages/coins/src/interfaces/IZoraFactory.sol

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,32 @@ interface IZoraFactory is IDeployedCoinVersionLookup {
9797
/// @notice Thrwon when an invalid config version is provided
9898
error InvalidConfig();
9999

100-
/// @notice Creates a new creator coin contract
100+
/// @dev Deprecated: use `deployCreatorCoin` instead that has a salt and post-deploy hook specified
101+
function deployCreatorCoin(
102+
address payoutRecipient,
103+
address[] memory owners,
104+
string memory uri,
105+
string memory name,
106+
string memory symbol,
107+
bytes memory poolConfig,
108+
address platformReferrer,
109+
bytes32 coinSalt
110+
) external returns (address);
111+
112+
/// @notice Creates a new creator coin contract with an optional hook that runs after the coin is deployed.
113+
/// Enables buying initial supply by supporting ETH transfers to the post-deploy hook.
101114
/// @param payoutRecipient The recipient of creator reward payouts; this can be updated by an owner
102115
/// @param owners The list of addresses that will be able to manage the coin's payout address and metadata uri
103116
/// @param uri The coin metadata uri
104117
/// @param name The name of the coin
105118
/// @param symbol The symbol of the coin
106119
/// @param poolConfig The config parameters for the coin's pool
107120
/// @param platformReferrer The address of the platform referrer
121+
/// @param postDeployHook The address of the hook to run after the coin is deployed
122+
/// @param postDeployHookData The data to pass to the hook
108123
/// @param coinSalt The salt used to deploy the coin
124+
/// @return coin The address of the deployed creator coin
125+
/// @return postDeployHookDataOut The data returned by the hook
109126
function deployCreatorCoin(
110127
address payoutRecipient,
111128
address[] memory owners,
@@ -114,8 +131,10 @@ interface IZoraFactory is IDeployedCoinVersionLookup {
114131
string memory symbol,
115132
bytes memory poolConfig,
116133
address platformReferrer,
134+
address postDeployHook,
135+
bytes calldata postDeployHookData,
117136
bytes32 coinSalt
118-
) external returns (address);
137+
) external payable returns (address coin, bytes memory postDeployHookDataOut);
119138

120139
/// @notice Creates a new coin contract with an optional hook that runs after the coin is deployed.
121140
/// Requires a salt to be specified, which enabled the coin to be deployed deterministically, and at

packages/coins/test/DeploymentHooks.t.sol

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity ^0.8.13;
44
import {BaseTest} from "./utils/BaseTest.sol";
55
import {BuySupplyWithSwapRouterHook} from "../src/hooks/deployment/BuySupplyWithSwapRouterHook.sol";
66
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7+
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
78
import {IUniswapV3Pool} from "../src/interfaces/IUniswapV3Pool.sol";
89
import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
910
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
@@ -211,4 +212,55 @@ contract DeploymentsHooksTest is BaseTest {
211212
vm.prank(users.creator);
212213
_deployWithHook(address(0), bytes(""), zora);
213214
}
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+
}
214266
}

0 commit comments

Comments
 (0)