Skip to content

Commit a244657

Browse files
authored
Merge pull request #2 from PartyDAO/feat/v3-positions
Create a new `ERC20Creator` that uses v3 positions
2 parents c5b42b7 + 62c15e8 commit a244657

24 files changed

+2320
-32
lines changed

.github/workflows/ci.yml

+11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@v2
1616

17+
- name: Use Node.js 16.x
18+
uses: actions/setup-node@v3
19+
with:
20+
node-version: 18.x
21+
22+
- name: Install Dependencies
23+
run: yarn install
24+
1725
- name: Install Foundry
1826
uses: foundry-rs/foundry-toolchain@v1
1927
with:
@@ -24,3 +32,6 @@ jobs:
2432

2533
- name: Run tests
2634
run: forge test -vvv --evm-version shanghai --fork-url ${{ secrets.SEPOLIA_FORK_URL }}
35+
36+
- name: Upload Selectors
37+
run: forge selectors upload --all

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
cache/
22
out/
3+
node_modules/

.gitmodules

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
[submodule "lib/forge-std"]
22
path = lib/forge-std
33
url = https://github.com/foundry-rs/forge-std
4-
[submodule "lib/openzeppelin-contracts"]
5-
path = lib/openzeppelin-contracts
6-
url = https://github.com/OpenZeppelin/openzeppelin-contracts
74
[submodule "lib/v2-core"]
85
path = lib/v2-core
96
url = https://github.com/Uniswap/v2-core
@@ -13,3 +10,15 @@
1310
[submodule "lib/party-protocol"]
1411
path = lib/party-protocol
1512
url = https://github.com/PartyDAO/party-protocol
13+
[submodule "lib/openzeppelin-contracts"]
14+
path = lib/openzeppelin-contracts
15+
url = https://github.com/OpenZeppelin/openzeppelin-contracts
16+
[submodule "lib/v3-core"]
17+
path = lib/v3-core
18+
url = https://github.com/Uniswap/v3-core
19+
[submodule "lib/old-oz"]
20+
path = lib/old-oz
21+
url = https://github.com/OpenZeppelin/openzeppelin-contracts
22+
[submodule "lib/v3-periphery"]
23+
path = lib/v3-periphery
24+
url = https://github.com/ChrisiPK/v3-periphery

lib/old-oz

Submodule old-oz added at c3178ff

lib/openzeppelin-contracts

lib/v3-core

Submodule v3-core added at e3589b1

lib/v3-periphery

Submodule v3-periphery added at a82c50e

remappings.txt

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@openzeppelin/contracts/=lib/old-oz/contracts/
2+
base64-sol/=node_modules/base64-sol/
3+
ds-test/=lib/party-protocol/lib/forge-std/lib/ds-test/src/
4+
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
5+
forge-std/=lib/forge-std/src/
6+
old-oz/=lib/old-oz/contracts/
7+
openzeppelin-contracts/=lib/openzeppelin-contracts/
8+
party-addresses/=lib/party-protocol/lib/
9+
party-protocol/=lib/party-protocol/
10+
v2-core/=lib/v2-core/contracts/
11+
v2-periphery/=lib/v2-periphery/contracts/
12+
v3-core/=lib/v3-core/
13+
v3-periphery/=lib/v3-periphery/contracts/

src/ERC20Creator.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.0;
33

44
import "../lib/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
55
import "../lib/v2-core/contracts/interfaces/IUniswapV2Factory.sol";
6-
import "./../lib/party-protocol/contracts/distribution/ITokenDistributor.sol";
6+
import "party-protocol/contracts/distribution/ITokenDistributor.sol";
77
import {GovernableERC20, ERC20} from "./GovernableERC20.sol";
88

99
contract ERC20Creator {

src/ERC20CreatorV3.sol

+310
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol";
5+
import {INonfungiblePositionManager} from "v3-periphery/interfaces/INonfungiblePositionManager.sol";
6+
import {IMulticall} from "v3-periphery/interfaces/IMulticall.sol";
7+
import {IUniswapV3Factory} from "v3-core/contracts/interfaces/IUniswapV3Factory.sol";
8+
import {IUniswapV3Pool} from "v3-core/contracts/interfaces/IUniswapV3Pool.sol";
9+
import {IERC721Receiver} from "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol";
10+
import {ITokenDistributor, IERC20, Party} from "party-protocol/contracts/distribution/ITokenDistributor.sol";
11+
import {GovernableERC20} from "./GovernableERC20.sol";
12+
import {FeeRecipient} from "./FeeCollector.sol";
13+
14+
contract ERC20CreatorV3 is IERC721Receiver {
15+
struct TokenDistributionConfiguration {
16+
uint256 totalSupply; // Total supply of the token
17+
uint256 numTokensForDistribution; // Number of tokens to distribute to the party
18+
uint256 numTokensForRecipient; // Number of tokens to send to the `tokenRecipient`
19+
uint256 numTokensForLP; // Number of tokens for the Uniswap V3 LP
20+
}
21+
22+
event ERC20Created(
23+
address indexed token,
24+
address indexed party,
25+
address indexed recipient,
26+
string name,
27+
string symbol,
28+
uint256 ethValue,
29+
TokenDistributionConfiguration config
30+
);
31+
32+
event FeeRecipientUpdated(
33+
address indexed oldFeeRecipient,
34+
address indexed newFeeRecipient
35+
);
36+
37+
event FeeBasisPointsUpdated(
38+
uint16 oldFeeBasisPoints,
39+
uint16 newFeeBasisPoints
40+
);
41+
42+
error InvalidTokenDistribution();
43+
error OnlyFeeRecipient();
44+
error InvalidPoolFee();
45+
error InvalidFeeBasisPoints();
46+
47+
address public immutable WETH;
48+
INonfungiblePositionManager public immutable UNISWAP_V3_POSITION_MANAGER;
49+
IUniswapV3Factory public immutable UNISWAP_V3_FACTORY;
50+
/// @dev Helper constant for calculating sqrtPriceX96
51+
uint256 private constant _X96 = 2 ** 96;
52+
53+
/// @notice Fee Collector address. All LP positions transferred here
54+
address public immutable FEE_COLLECTOR;
55+
/// @notice Uniswap V3 pool fee in hundredths of a bip
56+
uint24 public immutable POOL_FEE;
57+
/// @notice The maxTick for the given pool fee
58+
int24 public immutable MAX_TICK;
59+
/// @notice The minTick for the given pool fee
60+
int24 public immutable MIN_TICK;
61+
/// @notice PartyDao token distributor contract
62+
ITokenDistributor public immutable TOKEN_DISTRIBUTOR;
63+
64+
/// @notice Address that receives fee split of ETH at LP creation
65+
address public feeRecipient;
66+
/// @notice Fee basis points for ETH split on LP creation
67+
uint16 public feeBasisPoints;
68+
69+
/// @param tokenDistributor PartyDao token distributor contract
70+
/// @param uniswapV3PositionManager Uniswap V3 position manager contract
71+
/// @param uniswapV3Factory Uniswap V3 factory contract
72+
/// @param feeCollector Fee collector address which v3 lp positions are transferred to.
73+
/// @param weth WETH address
74+
/// @param feeRecipient_ Address that receives fee split of ETH at LP creation
75+
/// @param feeBasisPoints_ Fee basis points for ETH split on LP creation
76+
/// @param poolFee Uniswap V3 pool fee in hundredths of a bip
77+
constructor(
78+
ITokenDistributor tokenDistributor,
79+
INonfungiblePositionManager uniswapV3PositionManager,
80+
IUniswapV3Factory uniswapV3Factory,
81+
address feeCollector,
82+
address weth,
83+
address feeRecipient_,
84+
uint16 feeBasisPoints_,
85+
uint16 poolFee
86+
) {
87+
if (poolFee != 500 && poolFee != 3000 && poolFee != 10_000)
88+
revert InvalidPoolFee();
89+
if (feeBasisPoints_ > 5e3) revert InvalidFeeBasisPoints();
90+
91+
TOKEN_DISTRIBUTOR = tokenDistributor;
92+
UNISWAP_V3_POSITION_MANAGER = uniswapV3PositionManager;
93+
UNISWAP_V3_FACTORY = uniswapV3Factory;
94+
WETH = weth;
95+
feeRecipient = feeRecipient_;
96+
feeBasisPoints = feeBasisPoints_;
97+
POOL_FEE = poolFee;
98+
FEE_COLLECTOR = feeCollector;
99+
100+
int24 tickSpacing = UNISWAP_V3_FACTORY.feeAmountTickSpacing(POOL_FEE);
101+
MAX_TICK = (887272 /* TickMath.MAX_TICK */ / tickSpacing) * tickSpacing;
102+
MIN_TICK =
103+
(-887272 /* TickMath.MIN_TICK */ / tickSpacing) *
104+
tickSpacing;
105+
}
106+
107+
/// @notice Creates a new ERC20 token, LPs it in a locked full range Uniswap V3 position, and distributes some of the new token to party members.
108+
/// @dev The party is assumed to be `msg.sender`
109+
/// @param party The party to allocate this token to
110+
/// @param name The name of the new token
111+
/// @param symbol The symbol of the new token
112+
/// @param config Token distribution configuration. See above for additional information.
113+
/// @param tokenRecipientAddress The address to receive the tokens allocated for the token recipient
114+
/// @return token The address of the newly created token
115+
function createToken(
116+
address party,
117+
string memory name,
118+
string memory symbol,
119+
TokenDistributionConfiguration memory config,
120+
address tokenRecipientAddress
121+
) external payable returns (address) {
122+
// Require that tokens are fully distributed
123+
if (
124+
config.numTokensForDistribution +
125+
config.numTokensForRecipient +
126+
config.numTokensForLP !=
127+
config.totalSupply ||
128+
config.totalSupply > type(uint112).max
129+
) {
130+
revert InvalidTokenDistribution();
131+
}
132+
133+
// We use a changing salt to ensure address changes every block. If the LP position already exists, the TX will revert.
134+
// Can be tried again the next block.
135+
IERC20 token = IERC20(
136+
address(
137+
new GovernableERC20{
138+
salt: keccak256(
139+
abi.encode(blockhash(block.number - 1), msg.sender)
140+
)
141+
}(name, symbol, config.totalSupply, address(this))
142+
)
143+
);
144+
145+
if (config.numTokensForDistribution > 0) {
146+
// Create distribution
147+
token.transfer(
148+
address(TOKEN_DISTRIBUTOR),
149+
config.numTokensForDistribution
150+
);
151+
TOKEN_DISTRIBUTOR.createErc20Distribution(
152+
token,
153+
Party(payable(party)),
154+
payable(address(0)),
155+
0
156+
);
157+
}
158+
159+
// Take fee
160+
uint256 feeAmount = (msg.value * feeBasisPoints) / 1e4;
161+
162+
// The id of the LP nft
163+
uint256 lpTokenId;
164+
165+
{
166+
(address token0, address token1) = WETH < address(token)
167+
? (WETH, address(token))
168+
: (address(token), WETH);
169+
(uint256 amount0, uint256 amount1) = WETH < address(token)
170+
? (msg.value - feeAmount, config.numTokensForLP)
171+
: (config.numTokensForLP, msg.value - feeAmount);
172+
173+
// Create and initialize pool. Reverts if pool already created.
174+
address pool = UNISWAP_V3_FACTORY.createPool(
175+
address(token),
176+
WETH,
177+
POOL_FEE
178+
);
179+
180+
// Initialize pool for the derived starting price
181+
uint160 sqrtPriceX96 = uint160(
182+
(Math.sqrt((amount1 * 1e18) / amount0) * _X96) / 1e9
183+
);
184+
IUniswapV3Pool(pool).initialize(sqrtPriceX96);
185+
186+
token.approve(
187+
address(UNISWAP_V3_POSITION_MANAGER),
188+
config.numTokensForLP
189+
);
190+
191+
// Use multicall to sweep back excess ETH
192+
bytes[] memory calls = new bytes[](2);
193+
calls[0] = abi.encodeCall(
194+
UNISWAP_V3_POSITION_MANAGER.mint,
195+
(
196+
INonfungiblePositionManager.MintParams({
197+
token0: token0,
198+
token1: token1,
199+
fee: POOL_FEE,
200+
tickLower: MIN_TICK,
201+
tickUpper: MAX_TICK,
202+
amount0Desired: amount0,
203+
amount1Desired: amount1,
204+
amount0Min: 0,
205+
amount1Min: 0,
206+
recipient: address(this),
207+
deadline: block.timestamp
208+
})
209+
)
210+
);
211+
calls[1] = abi.encodePacked(
212+
UNISWAP_V3_POSITION_MANAGER.refundETH.selector
213+
);
214+
bytes memory mintReturnData = IMulticall(
215+
address(UNISWAP_V3_POSITION_MANAGER)
216+
).multicall{value: msg.value - feeAmount}(calls)[0];
217+
218+
lpTokenId = abi.decode(mintReturnData, (uint256));
219+
}
220+
221+
// Transfer tokens to token recipient
222+
if (config.numTokensForRecipient > 0) {
223+
token.transfer(tokenRecipientAddress, config.numTokensForRecipient);
224+
}
225+
226+
// Refund any remaining dust of the token to the party
227+
{
228+
uint256 remainingTokenBalance = token.balanceOf(address(this));
229+
if (remainingTokenBalance > 0) {
230+
// Adjust the numTokensForLP to reflect the actual amount used
231+
config.numTokensForLP -= remainingTokenBalance;
232+
token.transfer(party, remainingTokenBalance);
233+
}
234+
}
235+
236+
// Transfer fee
237+
if (feeAmount > 0) {
238+
feeRecipient.call{value: feeAmount, gas: 100_000}("");
239+
}
240+
241+
// Transfer remaining ETH to the party
242+
if (address(this).balance > 0) {
243+
payable(party).call{value: address(this).balance, gas: 100_000}("");
244+
}
245+
246+
FeeRecipient[] memory recipients = new FeeRecipient[](1);
247+
recipients[0] = FeeRecipient({
248+
recipient: payable(party),
249+
percentageBps: 10_000
250+
});
251+
252+
// Transfer LP to fee collector contract
253+
UNISWAP_V3_POSITION_MANAGER.safeTransferFrom(
254+
address(this),
255+
FEE_COLLECTOR,
256+
lpTokenId,
257+
abi.encode(recipients)
258+
);
259+
260+
emit ERC20Created(
261+
address(token),
262+
party,
263+
tokenRecipientAddress,
264+
name,
265+
symbol,
266+
msg.value,
267+
config
268+
);
269+
270+
return address(token);
271+
}
272+
273+
/// @notice Get the Uniswap V3 pool for a token
274+
function getPool(address token) external view returns (address) {
275+
return UNISWAP_V3_FACTORY.getPool(token, WETH, POOL_FEE);
276+
}
277+
278+
/// @notice Sets the fee recipient for ETH split on LP creation
279+
function setFeeRecipient(address feeRecipient_) external {
280+
address oldFeeRecipient = feeRecipient;
281+
282+
if (msg.sender != oldFeeRecipient) revert OnlyFeeRecipient();
283+
feeRecipient = feeRecipient_;
284+
285+
emit FeeRecipientUpdated(oldFeeRecipient, feeRecipient_);
286+
}
287+
288+
/// @notice Sets the fee basis points for ETH split on LP creation
289+
/// @param feeBasisPoints_ The new fee basis points in basis points
290+
function setFeeBasisPoints(uint16 feeBasisPoints_) external {
291+
if (msg.sender != feeRecipient) revert OnlyFeeRecipient();
292+
if (feeBasisPoints_ > 5e3) revert InvalidFeeBasisPoints();
293+
emit FeeBasisPointsUpdated(feeBasisPoints, feeBasisPoints_);
294+
295+
feeBasisPoints = feeBasisPoints_;
296+
}
297+
298+
/// @notice Allow contract to receive refund from position manager
299+
receive() external payable {}
300+
301+
/// @notice Allow for Uniswap V3 lp position to be received
302+
function onERC721Received(
303+
address,
304+
address,
305+
uint256,
306+
bytes calldata
307+
) external pure returns (bytes4) {
308+
return IERC721Receiver.onERC721Received.selector;
309+
}
310+
}

0 commit comments

Comments
 (0)