Skip to content

Commit b0d4a83

Browse files
committed
fly-dex
1 parent 2bbbb11 commit b0d4a83

File tree

3 files changed

+237
-0
lines changed

3 files changed

+237
-0
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +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

lib/openzeppelin-contracts

Submodule openzeppelin-contracts added at efdc7cd

src/FlyDex.sol

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/*
2+
FEATURES:
3+
- ✅ Swap de cualquier token ERC20 por otro token ERC20 - swapExactTokensForTokens()
4+
- ✅ Swap de ETH por cualquier token ERC20 - swapExactTokensForETH()
5+
- ✅ Añadir liquidez de cualquier token ERC20 - addLiquidity()
6+
- ✅ Retirar liquidez de cualquier token ERC20 - removeLiquidity()
7+
- ✅ Añadir liquidez de ETH - addLiquidityETH()
8+
- ✅ Retirar liquidez de ETH - removeLiquidityETH()
9+
- ✅ Cobrar una fee por cada swap - ex 0.1%
10+
- ✅ Permitir al owner cambiar la fee - changeFee()
11+
- ✅ Permitir al owner retirar las fees acumuladas - withdrawFees()
12+
13+
- ✅ Emitir eventos para cada acción
14+
- ✅ Seguridad contra reentrancy con OpenZeppelin ReentrancyGuard --> nonReentrant
15+
- ✅ Testing completo con fuzzing y invariants
16+
17+
- Frontend (React + Vite + Viem + Wagmi + TailwindCSS):
18+
- Interfaz sencilla para interactuar con el contrato en la red de Arbitrum
19+
- Conexión con wallet (MetaMask, WalletConnect, etc)
20+
- Swap de algunos tokens (ETH, LINK, UNI, DAI, WETH, DOT, ARB)
21+
- Añadir y retirar liquidez ETH
22+
- Ver fees acumuladas y retirarlas (sólo owner)
23+
*/
24+
25+
// SPDX-License-Identifier: MIT
26+
27+
pragma solidity ^0.8.24;
28+
29+
30+
import "@openzeppelin/contracts/access/Ownable.sol";
31+
import "./interfaces/IV2Router.sol";
32+
import "./interfaces/IV2Factory.sol";
33+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
34+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
35+
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
36+
37+
contract FlyDex is Ownable, ReentrancyGuard {
38+
39+
using SafeERC20 for IERC20;
40+
41+
address WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; // WETH en Arbitrum
42+
address public immutable UniswapV2RouterAddress; // uniswap v2 router on Arbitrum
43+
address public immutable UniswapV2FactoryAddress; // uniswap v2 factory on Arbitrum
44+
45+
uint256 public fee; // fee en basis points (bps), ex 10 = 0.1%
46+
uint256 public constant MAX_FEE = 500; // max fee 5%
47+
mapping(address => uint256) public feesCollectedPerToken; // fees collected per token
48+
49+
event FeeChanged(uint256 newFee);
50+
event FeeCollected(uint256 fee);
51+
event SwapExecuted(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut);
52+
event SwapETHExecuted(address user, uint256 amountIn, address tokenOut, uint256 amountOut);
53+
event FeesWithdrawn(address token, uint256 amount, address to);
54+
event LiquidityTokensAdded(address tokenA, address tokenB, uint256 lpTokens);
55+
event LiquidityETHAdded(uint256 amountETH, address token, uint256 amountTokenDesired, uint256 lpTokens);
56+
event RemoveLiquidityTokens(address tokenA, address tokenB, uint256 liquidity, address to);
57+
event RemoveLiquidityETH(address token, uint256 liquidity, address to);
58+
59+
constructor(address _uniswapV2RouterAddress, address _uniswapV2FactoryAddress, uint256 _fee) Ownable(msg.sender) {
60+
UniswapV2RouterAddress = _uniswapV2RouterAddress;
61+
UniswapV2FactoryAddress = _uniswapV2FactoryAddress;
62+
fee = _fee;
63+
}
64+
65+
receive() external payable {}
66+
67+
/**
68+
* @param _newFee New fee in basis points (bps)
69+
*/
70+
function changeFee(uint256 _newFee) external onlyOwner {
71+
require(_newFee <= MAX_FEE, "Fee too high"); // max 5%
72+
fee = _newFee;
73+
74+
emit FeeChanged(_newFee);
75+
}
76+
77+
/**
78+
* @notice Function for the owner to withdraw accumulated fees
79+
* @param _token Address of the token to withdraw, use address(0) for ETH
80+
* @param _to Address to send the withdrawn fees to
81+
*/
82+
function withdrawFees(address _token, address _to) external onlyOwner nonReentrant {
83+
uint256 amount = feesCollectedPerToken[_token];
84+
require(amount > 0, "No fees to withdraw");
85+
feesCollectedPerToken[_token] = 0;
86+
87+
if (_token == address(0)) {
88+
// Withdraw ETH
89+
(bool success, ) = _to.call{value: amount}("");
90+
require(success, "ETH Transfer failed");
91+
} else {
92+
// Withdraw ERC20
93+
IERC20(_token).safeTransfer(_to, amount);
94+
}
95+
96+
emit FeesWithdrawn(_token, amount, _to);
97+
}
98+
99+
/**
100+
* @notice Function to swap any token ERC20 for another token ERC20
101+
* @param _tokenIn Address of the token to swap from
102+
* @param _tokenOut Address of the token to swap to
103+
* @param _amountIn Amount of tokenA to swap
104+
* @param _amountOutMin Minimum amount of tokenB to receive
105+
* @param _path Array of token addresses (path) for the swap
106+
* @param _deadline Unix timestamp after which the transaction will revert
107+
*/
108+
function swapTokens(address _tokenIn, address _tokenOut, uint256 _amountIn, uint256 _amountOutMin, address[] calldata _path, uint256 _deadline) external returns (uint256) {
109+
// Calculate fee and transfer tokens accordingly
110+
uint256 feeAmount = (_amountIn * fee) / 10000;
111+
uint256 amountAfterFee = _amountIn - feeAmount;
112+
feesCollectedPerToken[_tokenIn] += feeAmount;
113+
114+
// Swap tokens using Uniswap V2 Router
115+
IERC20(_tokenIn).safeTransferFrom(msg.sender, address(this), _amountIn);
116+
IERC20(_tokenIn).approve(UniswapV2RouterAddress, amountAfterFee);
117+
uint256[] memory amountsOut = IV2Router(UniswapV2RouterAddress).swapExactTokensForTokens(amountAfterFee, _amountOutMin, _path, msg.sender, _deadline);
118+
119+
// Emit event
120+
emit SwapExecuted(_tokenIn, _tokenOut, _amountIn, amountsOut[amountsOut.length - 1]);
121+
emit FeeCollected(feeAmount);
122+
123+
return amountsOut[amountsOut.length - 1];
124+
}
125+
126+
/**
127+
* @notice Function to swap ETH for any ERC20 token
128+
* @param _tokenOut Address of the token to swap to
129+
* @param _amountOutMin Minimum amount of token to receive
130+
* @param _path Array of token addresses (path) for the swap, must start with WETH
131+
* @param _deadline Unix timestamp after which the transaction will revert
132+
*/
133+
function swapETHForTokens(address _tokenOut, uint256 _amountOutMin, address[] calldata _path, uint256 _deadline) external payable returns (uint256) {
134+
require(msg.value > 0, "Must send ETH");
135+
136+
// Calculate fee and transfer accordingly
137+
uint256 feeAmount = (msg.value * fee) / 10000;
138+
uint256 amountAfterFee = msg.value - feeAmount;
139+
feesCollectedPerToken[address(0)] += feeAmount; // address(0) for ETH
140+
141+
uint256[] memory amounts = IV2Router(UniswapV2RouterAddress).swapExactETHForTokens{value: amountAfterFee}(_amountOutMin, _path, msg.sender, _deadline);
142+
143+
emit SwapETHExecuted(msg.sender, msg.value, _tokenOut, amounts[amounts.length - 1]);
144+
emit FeeCollected(feeAmount);
145+
146+
return amounts[amounts.length - 1];
147+
}
148+
149+
/**
150+
* @notice Function to add liquidity for any ERC20 token pair
151+
* @param _tokenA Address of token A
152+
* @param _tokenB Address of token B
153+
* @param _amountADesired Amount of token A to add as liquidity
154+
* @param _amountBDesired Amount of token B to add as liquidity
155+
* @param _amountAMin Minimum amount of token A to add (slippage protection)
156+
* @param _amountBMin Minimum amount of token B to add (slippage protection)
157+
* @param _deadline Unix timestamp after which the transaction will revert
158+
*/
159+
function addLiquidityTokens(address _tokenA, address _tokenB, uint256 _amountADesired, uint256 _amountBDesired, uint256 _amountAMin, uint256 _amountBMin, uint256 _deadline) external returns (uint256) {
160+
require(_amountADesired > 0 && _amountBDesired > 0, "Amounts must be greater than 0");
161+
require(_tokenA != _tokenB, "Tokens must be different");
162+
163+
IERC20(_tokenA).safeTransferFrom(msg.sender, address(this), _amountADesired);
164+
IERC20(_tokenB).safeTransferFrom(msg.sender, address(this), _amountBDesired);
165+
IERC20(_tokenA).approve(UniswapV2RouterAddress, _amountADesired);
166+
IERC20(_tokenB).approve(UniswapV2RouterAddress, _amountBDesired);
167+
(,, uint256 lpTokens) = IV2Router(UniswapV2RouterAddress).addLiquidity(_tokenA, _tokenB, _amountADesired, _amountBDesired, _amountAMin, _amountBMin, msg.sender, _deadline);
168+
169+
emit LiquidityTokensAdded(_tokenA, _tokenB, lpTokens);
170+
171+
return lpTokens;
172+
}
173+
174+
/**
175+
* @notice Function to add liquidity with ETH
176+
* @param _token Address of the ERC20 token to pair with ETH
177+
* @param _amountTokenDesired Amount of the ERC20 token to add as liquidity
178+
* @param _amountTokenMin Minimum amount of the ERC20 token to add (slippage protection)
179+
* @param _amountETHMin Minimum amount of ETH to add (slippage protection)
180+
* @param _deadline Unix timestamp after which the transaction will revert
181+
*/
182+
function addLiquidityETH(address _token, uint256 _amountTokenDesired, uint256 _amountTokenMin, uint256 _amountETHMin, uint256 _deadline) external payable returns (uint256) {
183+
require(msg.value > 0, "Must send ETH");
184+
require(_token != WETH, "Token cannot be WETH");
185+
186+
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amountTokenDesired);
187+
IERC20(_token).approve(UniswapV2RouterAddress, _amountTokenDesired);
188+
(,, uint256 lpToken) = IV2Router(UniswapV2RouterAddress).addLiquidityETH{value: msg.value}(_token, _amountTokenDesired, _amountTokenMin, _amountETHMin, msg.sender, _deadline);
189+
190+
emit LiquidityETHAdded(msg.value, _token, _amountTokenDesired, lpToken);
191+
192+
return lpToken;
193+
}
194+
195+
/**
196+
* @notice Function to remove liquidity for any ERC20 token pair
197+
* @param _tokenA Address of token A
198+
* @param _tokenB Address of token B
199+
* @param _liquidity Amount of liquidity tokens to remove
200+
* @param _amountAMin Minimum amount of token A to receive (slippage protection)
201+
* @param _amountBMin Minimum amount of token B to receive (slippage protection)
202+
* @param _deadline Unix timestamp after which the transaction will revert
203+
*/
204+
function removeLiquidity(address _tokenA, address _tokenB, uint256 _liquidity, uint256 _amountAMin, uint256 _amountBMin, uint256 _deadline) external {
205+
require(_tokenA != _tokenB, "Tokens must be different");
206+
207+
address pairAddr = IV2Factory(UniswapV2FactoryAddress).getPair(_tokenA, _tokenB);
208+
IERC20(pairAddr).safeTransferFrom(msg.sender, address(this), _liquidity);
209+
IERC20(pairAddr).approve(UniswapV2RouterAddress, _liquidity);
210+
IV2Router(UniswapV2RouterAddress).removeLiquidity(_tokenA, _tokenB, _liquidity, _amountAMin, _amountBMin, msg.sender, _deadline);
211+
212+
emit RemoveLiquidityTokens(_tokenA, _tokenB, _liquidity, msg.sender);
213+
}
214+
215+
/**
216+
* @notice Function to remove liquidity with ETH
217+
* @param _token Address of the ERC20 token paired with ETH
218+
* @param _liquidity Amount of liquidity tokens to remove
219+
* @param _amountTokenMin Minimum amount of the ERC20 token to receive (slippage protection)
220+
* @param _amountETHMin Minimum amount of ETH to receive (slippage protection)
221+
* @param _deadline Unix timestamp after which the transaction will revert
222+
*/
223+
function removeLiquidityETH(address _token, uint256 _liquidity, uint256 _amountTokenMin, uint256 _amountETHMin, uint256 _deadline) external {
224+
require(_token != address(0), "Invalid token");
225+
226+
address pairAddr = IV2Factory(UniswapV2FactoryAddress).getPair(WETH, _token);
227+
IERC20(pairAddr).safeTransferFrom(msg.sender, address(this), _liquidity);
228+
IERC20(pairAddr).approve(UniswapV2RouterAddress, _liquidity);
229+
IV2Router(UniswapV2RouterAddress).removeLiquidityETH(_token, _liquidity, _amountTokenMin, _amountETHMin, msg.sender, _deadline);
230+
231+
emit RemoveLiquidityETH(_token, _liquidity, msg.sender);
232+
}
233+
}

0 commit comments

Comments
 (0)