Skip to content
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

Merged
merged 23 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
800617a
added _mintFullRange feature
potvik Nov 28, 2024
9305079
Added liquidity manager
potvik Dec 1, 2024
f7f508f
forge install: v3-periphery
polymorpher Dec 3, 2024
bf3af98
gitignore
polymorpher Dec 3, 2024
999d043
review fixes
potvik Dec 4, 2024
08426af
added _buyReceivedAmount and _sellReceivedAmount
potvik Dec 5, 2024
c666cbf
Merge branch 'uniswap_liquidity_manager' into polymorpher/uniswap_liq…
polymorpher Dec 6, 2024
6f1166a
forge install: v3-core
polymorpher Dec 7, 2024
521b6b2
fix compile issues and add v3 remapping
polymorpher Dec 7, 2024
c617cb1
simplify and fix fee calculation, _buy logic
polymorpher Dec 7, 2024
8649efe
Fix fee issues in _sell. Fix event emission fee field error. Remove T…
polymorpher Dec 7, 2024
41e735f
prettier
polymorpher Dec 7, 2024
68fb318
fix getCollateralByCompetitionId to account for fees for selling. Sim…
polymorpher Dec 7, 2024
d5a1c10
fix liquidity computation issues with publishToUniswap; fix compile bug
polymorpher Dec 7, 2024
2ba6a57
prettier
polymorpher Dec 7, 2024
a3ac50d
add fee withdrawing
polymorpher Dec 8, 2024
4c330e6
createToken returns Token instead of address
polymorpher Dec 8, 2024
da4fe0a
Merge pull request #13 from polymorpher/polymorpher/uniswap_liquidity…
potvik Dec 8, 2024
e3b8e2e
fix revert
polymorpher Dec 9, 2024
498e279
Merge branch 'polymorpher/uniswap_liquidity_manager' into polymorpher…
polymorpher Dec 9, 2024
0c0f7e4
test skeleton
polymorpher Dec 9, 2024
32cf346
Merge pull request #14 from polymorpher/polymorpher/uniswap_liquidity…
potvik Dec 9, 2024
07ae327
Merge pull request #15 from polymorpher/polymorpher/e2e-test
potvik Dec 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions contracts/LiquidityManager.sol
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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WETH

address public nonfungiblePositionManager;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INonfungiblePositionManager

address public uniswapV3Factory;

Choose a reason for hiding this comment

The 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) {

Choose a reason for hiding this comment

The 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}();
Copy link

@polymorpher polymorpher Dec 2, 2024

Choose a reason for hiding this comment

The 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));

Choose a reason for hiding this comment

The 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 ethAmount * 2 ** 192 overflow. I suggest scaling it by 96 bits only (which gives 2^96 more "space" to prevent overflow) before taking the division and sqrt, then scaling it by another 48 bits afterwards

_nonfungiblePositionManager.createAndInitializePoolIfNecessary(tokenAddress, WETH, UNISWAP_FEE, sqrtPriceX96);

Choose a reason for hiding this comment

The 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({
Copy link

@polymorpher polymorpher Dec 2, 2024

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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,

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

Choose a reason for hiding this comment

The 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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be close to tokenAmount and ethAmount, as sanity check

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

amount1Min: 0,
recipient: recipient,
deadline: block.timestamp
});

return _nonfungiblePositionManager.mint(params);
}
}
153 changes: 87 additions & 66 deletions contracts/TokenFactory.sol
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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 {
Expand Down Expand Up @@ -143,6 +162,7 @@ contract TokenFactory is ReentrancyGuard, Ownable {
tokensByCompetitionId[currentCompetitionId].push(tokenAddress);

competitionIds[tokenAddress] = currentCompetitionId;
tokensCreators[tokenAddress] = msg.sender;

emit TokenCreated(
tokenAddress,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {

Choose a reason for hiding this comment

The 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);
Copy link

@polymorpher polymorpher Dec 2, 2024

Choose a reason for hiding this comment

The 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);

Choose a reason for hiding this comment

The 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
);
}
}
}
32 changes: 32 additions & 0 deletions contracts/interfaces/IERC721Permit.sol
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;
}
Loading