Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title SimpleDEX
* @dev A simple decentralized exchange (DEX) implementation using constant product AMM (x*y=k)
* @notice This contract allows users to swap between two ERC20 tokens and provide liquidity
*
* Educational contract for Conflux Ukraine Webinar
* "DeFi на Conflux: Створення Simple DEX"
*/
contract SimpleDEX is ERC20, ReentrancyGuard {
// Token addresses for the pair
IERC20 public immutable token0;
IERC20 public immutable token1;
// Reserves for each token
uint256 public reserve0;
uint256 public reserve1;
// Fee percentage (0.3% = 30 basis points)
uint256 public constant FEE_PERCENT = 30; // 0.3%
uint256 public constant FEE_DENOMINATOR = 10000;
// Minimum liquidity to prevent pool share price manipulation
uint256 private constant MINIMUM_LIQUIDITY = 1000;
// Events
event LiquidityAdded(
address indexed provider,
uint256 amount0,
uint256 amount1,
uint256 liquidity
);
event LiquidityRemoved(
address indexed provider,
uint256 amount0,
uint256 amount1,
uint256 liquidity
);
event Swap(
address indexed user,
address indexed tokenIn,
uint256 amountIn,
uint256 amountOut
);
/**
* @dev Constructor to initialize the DEX with two tokens
* @param _token0 Address of the first token
* @param _token1 Address of the second token
*/
constructor(address _token0, address _token1) ERC20("SimpleDEX LP Token", "SDEX-LP") {
require(_token0 != address(0) && _token1 != address(0), "Invalid token addresses");
require(_token0 != _token1, "Identical tokens");
token0 = IERC20(_token0);
token1 = IERC20(_token1);
}
/**
* @dev Add liquidity to the pool
* @param amount0 Amount of token0 to add
* @param amount1 Amount of token1 to add
* @return liquidity Amount of LP tokens minted
*/
function addLiquidity(uint256 amount0, uint256 amount1)
external
nonReentrant
returns (uint256 liquidity)
{
require(amount0 > 0 && amount1 > 0, "Invalid amounts");
// Transfer tokens from user
token0.transferFrom(msg.sender, address(this), amount0);
token1.transferFrom(msg.sender, address(this), amount1);
// Calculate liquidity to mint
uint256 totalSupply = totalSupply();
if (totalSupply == 0) {
// First liquidity provider - permanently lock MINIMUM_LIQUIDITY
liquidity = sqrt(amount0 * amount1);
_mint(address(0), MINIMUM_LIQUIDITY); // Burn first 1000 wei to prevent manipulation
liquidity = liquidity - MINIMUM_LIQUIDITY;
} else {
// Subsequent liquidity providers
liquidity = min(
(amount0 * totalSupply) / reserve0,
(amount1 * totalSupply) / reserve1
);
}
require(liquidity > 0, "Insufficient liquidity minted");
// Mint LP tokens to provider
_mint(msg.sender, liquidity);
// Update reserves
reserve0 += amount0;
reserve1 += amount1;
emit LiquidityAdded(msg.sender, amount0, amount1, liquidity);
}
/**
* @dev Remove liquidity from the pool
* @param liquidity Amount of LP tokens to burn
* @return amount0 Amount of token0 returned
* @return amount1 Amount of token1 returned
*/
function removeLiquidity(uint256 liquidity)
external
nonReentrant
returns (uint256 amount0, uint256 amount1)
{
require(liquidity > 0, "Invalid liquidity amount");
require(balanceOf(msg.sender) >= liquidity, "Insufficient LP tokens");
uint256 totalSupply = totalSupply();
// Calculate token amounts to return
amount0 = (liquidity * reserve0) / totalSupply;
amount1 = (liquidity * reserve1) / totalSupply;
require(amount0 > 0 && amount1 > 0, "Insufficient liquidity burned");
// Burn LP tokens
_burn(msg.sender, liquidity);
// Transfer tokens to user
token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
// Update reserves
reserve0 -= amount0;
reserve1 -= amount1;
emit LiquidityRemoved(msg.sender, amount0, amount1, liquidity);
}
/**
* @dev Swap tokens using constant product formula (x*y=k)
* @param tokenIn Address of input token
* @param amountIn Amount of input tokens
* @param minAmountOut Minimum amount of output tokens (slippage protection)
* @return amountOut Amount of output tokens received
*/
function swap(address tokenIn, uint256 amountIn, uint256 minAmountOut)
external
nonReentrant
returns (uint256 amountOut)
{
require(amountIn > 0, "Invalid input amount");
require(
tokenIn == address(token0) || tokenIn == address(token1),
"Invalid input token"
);
// Determine which token is input and which is output
bool isToken0 = tokenIn == address(token0);
(IERC20 tokenInContract, IERC20 tokenOutContract, uint256 reserveIn, uint256 reserveOut) =
isToken0
? (token0, token1, reserve0, reserve1)
: (token1, token0, reserve1, reserve0);
// Transfer input tokens from user
tokenInContract.transferFrom(msg.sender, address(this), amountIn);
// Calculate output amount with fee
// Formula: amountOut = (amountIn * 0.997 * reserveOut) / (reserveIn + amountIn * 0.997)
uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_PERCENT);
amountOut = (amountInWithFee * reserveOut) / (reserveIn * FEE_DENOMINATOR + amountInWithFee);
require(amountOut >= minAmountOut, "Slippage exceeded");
require(amountOut > 0, "Insufficient output amount");
// Transfer output tokens to user
tokenOutContract.transfer(msg.sender, amountOut);
// Update reserves
if (isToken0) {
reserve0 += amountIn;
reserve1 -= amountOut;
} else {
reserve1 += amountIn;
reserve0 -= amountOut;
}
emit Swap(msg.sender, tokenIn, amountIn, amountOut);
}
/**
* @dev Get the amount of output tokens for a given input
* @param tokenIn Address of input token
* @param amountIn Amount of input tokens
* @return amountOut Expected amount of output tokens
*/
function getAmountOut(address tokenIn, uint256 amountIn)
external
view
returns (uint256 amountOut)
{
require(amountIn > 0, "Invalid input amount");
require(
tokenIn == address(token0) || tokenIn == address(token1),
"Invalid input token"
);
bool isToken0 = tokenIn == address(token0);
(uint256 reserveIn, uint256 reserveOut) = isToken0
? (reserve0, reserve1)
: (reserve1, reserve0);
uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_PERCENT);
amountOut = (amountInWithFee * reserveOut) / (reserveIn * FEE_DENOMINATOR + amountInWithFee);
}
/**
* @dev Get current price of token0 in terms of token1
* @return price Price of token0 (scaled by 1e18 for precision)
*/
function getPrice0() external view returns (uint256 price) {
require(reserve0 > 0 && reserve1 > 0, "No liquidity");
price = (reserve1 * 1e18) / reserve0;
}
/**
* @dev Get current price of token1 in terms of token0
* @return price Price of token1 (scaled by 1e18 for precision)
*/
function getPrice1() external view returns (uint256 price) {
require(reserve0 > 0 && reserve1 > 0, "No liquidity");
price = (reserve0 * 1e18) / reserve1;
}
// Helper functions
function sqrt(uint256 y) internal pure returns (uint256 z) {
if (y > 3) {
z = y;
uint256 x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
}