-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uniswap liquidity manager #12
Changes from 2 commits
800617a
9305079
f7f508f
bf3af98
999d043
08426af
c666cbf
6f1166a
521b6b2
c617cb1
8649efe
41e735f
68fb318
d5a1c10
2ba6a57
a3ac50d
4c330e6
da4fe0a
e3b8e2e
498e279
0c0f7e4
32cf346
07ae327
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.13; | ||
|
||
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; | ||
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; | ||
import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol"; | ||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
import {Token} from "./Token.sol"; | ||
|
||
interface IWETH is IERC20 { | ||
function deposit() external payable; | ||
function withdraw(uint) external; | ||
} | ||
|
||
interface IERC721Receiver { | ||
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4); | ||
} | ||
|
||
abstract contract LiquidityManager is IERC721Receiver, Ownable { | ||
uint24 public constant UNISWAP_FEE = 3000; | ||
int24 private constant MIN_TICK = -887272; | ||
int24 private constant MAX_TICK = -MIN_TICK; | ||
int24 private constant TICK_SPACING = 60; | ||
|
||
address internal immutable WETH; | ||
address public nonfungiblePositionManager; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. INonfungiblePositionManager |
||
address public uniswapV3Factory; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IUniswapV3Factory |
||
|
||
constructor(address _uniswapV3Factory, address _nonfungiblePositionManager, address _weth) Ownable(msg.sender) { | ||
uniswapV3Factory = _uniswapV3Factory; | ||
nonfungiblePositionManager = _nonfungiblePositionManager; | ||
WETH = _weth; | ||
} | ||
|
||
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata) external returns (bytes4) { | ||
return IERC721Receiver.onERC721Received.selector; | ||
} | ||
|
||
function _createLiquilityPool(address tokenAddress) internal returns (address) { | ||
IUniswapV3Factory factory = IUniswapV3Factory(uniswapV3Factory); | ||
|
||
address pool = factory.createPool(tokenAddress, WETH, UNISWAP_FEE); | ||
|
||
return pool; | ||
} | ||
|
||
function sqrt(uint256 x) internal pure returns (uint256) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use sqrt function in UD60X18 from prb-math instead? It is already installed in foundry dependency. Same approximation method, but checks for overflow, uses better initialization value, and limit the number of approximation steps |
||
if (x == 0) return 0; | ||
uint256 z = (x + 1) / 2; | ||
uint256 y = x; | ||
while (z < y) { | ||
y = z; | ||
z = (x / z + z) / 2; | ||
} | ||
return y; | ||
} | ||
|
||
function _addLiquidity( | ||
address tokenAddress, | ||
uint256 tokenAmount, | ||
uint256 ethAmount, | ||
address recipient | ||
) internal returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) { | ||
INonfungiblePositionManager _nonfungiblePositionManager = INonfungiblePositionManager(nonfungiblePositionManager); | ||
|
||
IUniswapV3Factory factory = IUniswapV3Factory(uniswapV3Factory); | ||
address poolAddress = factory.getPool(tokenAddress, WETH, UNISWAP_FEE); | ||
|
||
Token token = Token(tokenAddress); | ||
token.approve(nonfungiblePositionManager, tokenAmount); | ||
|
||
uint256 eth = address(this).balance; | ||
IWETH(WETH).deposit{value: eth}(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should not deposit everything to WETH. Just check if approved amount is already greater than what's needed. If there is a shortage, approve and deposit that amount |
||
IWETH(WETH).approve(nonfungiblePositionManager, eth); | ||
|
||
uint160 sqrtPriceX96 = uint160(sqrt((ethAmount * 2 ** 192) / tokenAmount)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will overflow easily, since 1e18 ~= 2^60, so any amount of native token greater than 2^(256-192-60) = 2^14 = 16384 would make |
||
_nonfungiblePositionManager.createAndInitializePoolIfNecessary(tokenAddress, WETH, UNISWAP_FEE, sqrtPriceX96); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Method does not exist. Just call initialize with sqrtPriceX96? Pool should be already created |
||
|
||
INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The convention (used in Uniswap code) is to order [token0,token1] addresses based on their hexadecimal value, and to have the lower valued address as token0. The pool does it automatically upon creation, so the parameters here should follow the same convention as well There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved |
||
token0: tokenAddress, | ||
token1: WETH, | ||
fee: UNISWAP_FEE, | ||
tickLower: (MIN_TICK / TICK_SPACING) * TICK_SPACING, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The initial liquidity added shouldn't concentrated at around the tick for initial price. It should spread out (from MIN to MAX, or some reasonable price range) so everyone can trade the token regardless of its current price There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MIN_TICK and MAX_TICK are actually not divisible by TICK_SPACING. Rounding is needed |
||
tickUpper: (MAX_TICK / TICK_SPACING) * TICK_SPACING, | ||
amount0Desired: tokenAmount, | ||
amount1Desired: ethAmount, | ||
amount0Min: 0, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be close to tokenAmount and ethAmount, as sanity check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved |
||
amount1Min: 0, | ||
recipient: recipient, | ||
deadline: block.timestamp | ||
}); | ||
|
||
return _nonfungiblePositionManager.mint(params); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,16 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.13; | ||
|
||
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol"; | ||
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router01.sol"; | ||
import "@openzeppelin/contracts/proxy/Clones.sol"; | ||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; | ||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
|
||
import {LiquidityManager} from './LiquidityManager.sol'; | ||
import {BancorBondingCurve} from "./BancorBondingCurve.sol"; | ||
import {Token} from "./Token.sol"; | ||
|
||
contract TokenFactory is ReentrancyGuard, Ownable { | ||
contract TokenFactory is ReentrancyGuard, LiquidityManager { | ||
enum TokenState { | ||
NOT_CREATED, | ||
FUNDING, | ||
|
@@ -32,15 +31,16 @@ contract TokenFactory is ReentrancyGuard, Ownable { | |
uint256 public currentCompetitionId = 0; | ||
|
||
address public immutable tokenImplementation; | ||
address public uniswapV2Router; | ||
address public uniswapV2Factory; | ||
BancorBondingCurve public bondingCurve; | ||
uint256 public feePercent; // bp | ||
uint256 public fee; | ||
|
||
mapping(uint256 => address) public winners; | ||
mapping(uint256 => mapping(address => uint256)) public collateralById; | ||
|
||
mapping(address => address) public tokensCreators; | ||
mapping(address => address) public tokensPools; | ||
|
||
// Events | ||
event TokenCreated( | ||
address indexed token, | ||
|
@@ -52,7 +52,10 @@ contract TokenFactory is ReentrancyGuard, Ownable { | |
uint256 timestamp | ||
); | ||
|
||
event TokenLiqudityAdded(address indexed token, uint256 timestamp); | ||
event NewCompetitionStarted( | ||
uint256 competitionId, | ||
uint256 timestamp | ||
); | ||
|
||
event TokenBuy( | ||
address indexed token, | ||
|
@@ -86,16 +89,27 @@ contract TokenFactory is ReentrancyGuard, Ownable { | |
uint256 timestamp | ||
); | ||
|
||
event WinnerLiquidityAdded( | ||
address indexed tokenAddress, | ||
address indexed tokenCreator, | ||
address indexed pool, | ||
address sender, | ||
uint256 tokenId, | ||
uint128 liquidity, | ||
uint256 amount0, | ||
uint256 amount1, | ||
uint256 timestamp | ||
); | ||
|
||
constructor( | ||
address _tokenImplementation, | ||
address _uniswapV2Router, | ||
address _uniswapV2Factory, | ||
address _uniswapV3Factory, | ||
address _nonfungiblePositionManager, | ||
address _bondingCurve, | ||
address _weth, | ||
uint256 _feePercent | ||
) Ownable(msg.sender) { | ||
) LiquidityManager(_uniswapV3Factory, _nonfungiblePositionManager, _weth) { | ||
tokenImplementation = _tokenImplementation; | ||
uniswapV2Router = _uniswapV2Router; | ||
uniswapV2Factory = _uniswapV2Factory; | ||
bondingCurve = BancorBondingCurve(_bondingCurve); | ||
feePercent = _feePercent; | ||
} | ||
|
@@ -112,6 +126,11 @@ contract TokenFactory is ReentrancyGuard, Ownable { | |
|
||
function startNewCompetition() external onlyOwner { | ||
currentCompetitionId = currentCompetitionId + 1; | ||
|
||
emit NewCompetitionStarted( | ||
currentCompetitionId, | ||
block.timestamp | ||
); | ||
} | ||
|
||
function setBondingCurve(address _bondingCurve) external onlyOwner { | ||
|
@@ -143,6 +162,7 @@ contract TokenFactory is ReentrancyGuard, Ownable { | |
tokensByCompetitionId[currentCompetitionId].push(tokenAddress); | ||
|
||
competitionIds[tokenAddress] = currentCompetitionId; | ||
tokensCreators[tokenAddress] = msg.sender; | ||
|
||
emit TokenCreated( | ||
tokenAddress, | ||
|
@@ -257,40 +277,6 @@ contract TokenFactory is ReentrancyGuard, Ownable { | |
|
||
// Internal functions | ||
|
||
function createLiquilityPool( | ||
address tokenAddress | ||
) internal returns (address) { | ||
IUniswapV2Factory factory = IUniswapV2Factory(uniswapV2Factory); | ||
IUniswapV2Router01 router = IUniswapV2Router01(uniswapV2Router); | ||
|
||
address pair = factory.createPair(tokenAddress, router.WETH()); | ||
return pair; | ||
} | ||
|
||
function addLiquidity( | ||
address tokenAddress, | ||
uint256 tokenAmount, | ||
uint256 ethAmount | ||
) internal returns (uint256) { | ||
Token token = Token(tokenAddress); | ||
IUniswapV2Router01 router = IUniswapV2Router01(uniswapV2Router); | ||
token.approve(uniswapV2Router, tokenAmount); | ||
//slither-disable-next-line arbitrary-send-eth | ||
(, , uint256 liquidity) = router.addLiquidityETH{value: ethAmount}( | ||
tokenAddress, | ||
tokenAmount, | ||
tokenAmount, | ||
ethAmount, | ||
address(this), | ||
block.timestamp | ||
); | ||
return liquidity; | ||
} | ||
|
||
function burnLiquidityToken(address pair, uint256 liquidity) internal { | ||
SafeERC20.safeTransfer(IERC20(pair), address(0), liquidity); | ||
} | ||
|
||
function calculateFee( | ||
uint256 _amount, | ||
uint256 _feePercent | ||
|
@@ -335,28 +321,63 @@ contract TokenFactory is ReentrancyGuard, Ownable { | |
|
||
address winnerToken = getWinnerByCompetitionId(_competitionId); | ||
|
||
require(winnerToken != tokenAddress, "token address is the winner"); | ||
|
||
Token token = Token(tokenAddress); | ||
uint256 burnedAmount = token.balanceOf(msg.sender); | ||
|
||
uint256 receivedETH = _sell( | ||
tokenAddress, | ||
burnedAmount, | ||
msg.sender, | ||
address(this) | ||
); | ||
|
||
uint256 mintedAmount = _buy(winnerToken, msg.sender, receivedETH); | ||
|
||
emit BurnTokenAndMintWinner( | ||
msg.sender, | ||
tokenAddress, | ||
winnerToken, | ||
burnedAmount, | ||
receivedETH, | ||
mintedAmount, | ||
block.timestamp | ||
); | ||
if(winnerToken == tokenAddress) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a different function for "graduating to Uniswap"? Since the logic is completely different from burning losing tokens |
||
address pool = _createLiquilityPool(tokenAddress); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should check that a pool was not created before, and revert if it was |
||
|
||
tokensPools[tokenAddress] = pool; | ||
|
||
uint256 ethAmount = collateralById[_competitionId][tokenAddress]; | ||
|
||
address creator = tokensCreators[tokenAddress]; | ||
uint256 creatorBalance = token.balanceOf(creator); | ||
|
||
// burned all creator tokens | ||
token.burn(creator, creatorBalance); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Token creator's tokens shouldn't be burned and seized. Otherwise there is no incentive for them to create tokens and market that |
||
|
||
token.mint(address(this), INITIAL_SUPPLY); | ||
|
||
// token creator will get liquidity pull tokenId | ||
( | ||
uint256 tokenId, | ||
uint128 liquidity, | ||
uint256 amount0, | ||
uint256 amount1 | ||
) = _addLiquidity(tokenAddress, INITIAL_SUPPLY, ethAmount, tokensCreators[tokenAddress]); | ||
|
||
emit WinnerLiquidityAdded( | ||
tokenAddress, | ||
tokensCreators[tokenAddress], | ||
pool, | ||
msg.sender, | ||
tokenId, | ||
liquidity, | ||
amount0, | ||
amount1, | ||
block.timestamp | ||
); | ||
} else { | ||
uint256 burnedAmount = token.balanceOf(msg.sender); | ||
|
||
uint256 receivedETH = _sell( | ||
tokenAddress, | ||
burnedAmount, | ||
msg.sender, | ||
address(this) | ||
); | ||
|
||
uint256 mintedAmount = _buy(winnerToken, msg.sender, receivedETH); | ||
|
||
emit BurnTokenAndMintWinner( | ||
msg.sender, | ||
tokenAddress, | ||
winnerToken, | ||
burnedAmount, | ||
receivedETH, | ||
mintedAmount, | ||
block.timestamp | ||
); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// SPDX-License-Identifier: GPL-2.0-or-later | ||
pragma solidity ^0.8.13; | ||
|
||
import '@openzeppelin/contracts/token/ERC721/IERC721.sol'; | ||
|
||
/// @title ERC721 with permit | ||
/// @notice Extension to ERC721 that includes a permit function for signature based approvals | ||
interface IERC721Permit is IERC721 { | ||
/// @notice The permit typehash used in the permit signature | ||
/// @return The typehash for the permit | ||
function PERMIT_TYPEHASH() external pure returns (bytes32); | ||
|
||
/// @notice The domain separator used in the permit signature | ||
/// @return The domain seperator used in encoding of permit signature | ||
function DOMAIN_SEPARATOR() external view returns (bytes32); | ||
|
||
/// @notice Approve of a specific token ID for spending by spender via signature | ||
/// @param spender The account that is being approved | ||
/// @param tokenId The ID of the token that is being approved for spending | ||
/// @param deadline The deadline timestamp by which the call must be mined for the approve to work | ||
/// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` | ||
/// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` | ||
/// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` | ||
function permit( | ||
address spender, | ||
uint256 tokenId, | ||
uint256 deadline, | ||
uint8 v, | ||
bytes32 r, | ||
bytes32 s | ||
) external payable; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WETH