diff --git a/contracts/scripts/deploy.sh b/contracts/scripts/deploy.sh index 09910da..485ec99 100755 --- a/contracts/scripts/deploy.sh +++ b/contracts/scripts/deploy.sh @@ -2,12 +2,15 @@ set -e -PRIVATE_KEY=$PRIVATE_KEY +PRIVATE_KEY=$DEPLOYER_PRIVATE_KEY RPC_URL=$RPC_URL +PREMIUM_USER_ADDRESS=$PREMIUM_USER_ADDRESS +BASIC_USER_ADDRESS=$BASIC_USER_ADDRESS + # Validate environment variables -if [ -z "$PRIVATE_KEY" ]; then - echo "PRIVATE_KEY is not set" +if [ -z "$DEPLOYER_PRIVATE_KEY" ]; then + echo "DEPLOYER_PRIVATE_KEY is not set" exit 1 fi @@ -16,15 +19,27 @@ if [ -z "$RPC_URL" ]; then exit 1 fi +if [ -z "$PREMIUM_USER_ADDRESS" ]; then + echo "PREMIUM_USER_ADDRESS is not set" + exit 1 +fi + +if [ -z "$BASIC_USER_ADDRESS" ]; then + echo "BASIC_USER_ADDRESS is not set" + exit 1 +fi + extract_deployed_address() { # Read input from stdin and extract the address after "Deployed to: " grep "Deployed to:" | cut -d' ' -f3 } + get_address_from_private_key() { - cast wallet address --private-key $1 + cast wallet address --private-key $1 } +# List of users. The deployer is the VIP user. DEPLOYER_ADDRESS=$(get_address_from_private_key $PRIVATE_KEY) # Build everything @@ -35,12 +50,26 @@ dai_address=$(forge create --rpc-url $RPC_URL --private-key $PRIVATE_KEY --zksyn wbtc_address=$(forge create --rpc-url $RPC_URL --private-key $PRIVATE_KEY --zksync src/TestnetERC20Token.sol:TestnetERC20Token --constructor-args "WBTC" "WBTC" 18 | extract_deployed_address) # Mint tokens +## VIP User cast send --rpc-url $RPC_URL --private-key $PRIVATE_KEY $dai_address "mint(address,uint256)" $DEPLOYER_ADDRESS 1000000000000000000000000 # 1 million DAI cast send --rpc-url $RPC_URL --private-key $PRIVATE_KEY $wbtc_address "mint(address,uint256)" $DEPLOYER_ADDRESS 100000000000000000000000 # 100,000 WBTC +## Premium User +cast send --rpc-url $RPC_URL --private-key $PRIVATE_KEY $dai_address "mint(address,uint256)" $PREMIUM_USER_ADDRESS 100000000000000000000000 # 100,000 DAI +cast send --rpc-url $RPC_URL --private-key $PRIVATE_KEY $wbtc_address "mint(address,uint256)" $PREMIUM_USER_ADDRESS 10000000000000000000000 # 10,000 WBTC + +## Basic User +cast send --rpc-url $RPC_URL --private-key $PRIVATE_KEY $dai_address "mint(address,uint256)" $BASIC_USER_ADDRESS 10000000000000000000000 # 10,000 DAI +cast send --rpc-url $RPC_URL --private-key $PRIVATE_KEY $wbtc_address "mint(address,uint256)" $BASIC_USER_ADDRESS 1000000000000000000000 # 1,000 WBTC + # Deploy CPAMM cpamm_address=$(forge create --rpc-url $RPC_URL --private-key $PRIVATE_KEY --zksync src/CPAMM.sol:CPAMM --constructor-args $dai_address $wbtc_address | extract_deployed_address) +# Set user fee tiers +cast send --rpc-url $RPC_URL --private-key $PRIVATE_KEY $cpamm_address "setUserFeeTier(address,uint256)" $DEPLOYER_ADDRESS 2 +cast send --rpc-url $RPC_URL --private-key $PRIVATE_KEY $cpamm_address "setUserFeeTier(address,uint256)" $PREMIUM_USER_ADDRESS 1 +cast send --rpc-url $RPC_URL --private-key $PRIVATE_KEY $cpamm_address "setUserFeeTier(address,uint256)" $BASIC_USER_ADDRESS 0 + echo "DAI: $dai_address" echo "WBTC: $wbtc_address" echo "CPAMM: $cpamm_address" diff --git a/contracts/src/CPAMM.sol b/contracts/src/CPAMM.sol index 73564f4..5a74491 100644 --- a/contracts/src/CPAMM.sol +++ b/contracts/src/CPAMM.sol @@ -1,62 +1,90 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol"; contract CPAMM { - IERC20 public immutable token0; - IERC20 public immutable token1; - - uint256 public reserve0; - uint256 public reserve1; - - uint256 public totalSupply; - mapping(address => uint256) public balanceOf; - - constructor(address _token0, address _token1) { - token0 = IERC20(_token0); - token1 = IERC20(_token1); - } - - function getReserves() external view returns (uint256, uint256) { - return (reserve0, reserve1); - } - - function _mint(address _to, uint256 _amount) private { - balanceOf[_to] += _amount; - totalSupply += _amount; - } - - function _burn(address _from, uint256 _amount) private { - balanceOf[_from] -= _amount; - totalSupply -= _amount; - } - - function _update(uint256 _reserve0, uint256 _reserve1) private { - reserve0 = _reserve0; - reserve1 = _reserve1; - } - - function swap( - address _tokenIn, - uint256 _amountIn - ) external returns (uint256 amountOut) { - require( - _tokenIn == address(token0) || _tokenIn == address(token1), - "invalid token" - ); - require(_amountIn > 0, "amount in = 0"); - - bool isToken0 = _tokenIn == address(token0); - (IERC20 tokenIn, IERC20 tokenOut, uint256 reserveIn, uint256 reserveOut) = - isToken0 - ? (token0, token1, reserve0, reserve1) - : (token1, token0, reserve1, reserve0); - - tokenIn.transferFrom(msg.sender, address(this), _amountIn); - - /* + IERC20 public immutable token0; + IERC20 public immutable token1; + + uint256 public reserve0; + uint256 public reserve1; + + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + + // Fee tiers + uint256 private constant BASIC_FEE = 10; // 1% + uint256 private constant PREMIUM_FEE = 5; // 0.5% + uint256 private constant VIP_FEE = 3; // 0.3% + + // 0 = basic, 1 = premium, 2 = vip + mapping(address => uint256) public userFeeTier; + + // Swap limit tiers (in token units) + uint256 private constant BASIC_LIMIT = 1000 * 1e18; // 1,000 tokens + uint256 private constant PREMIUM_LIMIT = 5000 * 1e18; // 5,000 tokens + uint256 private constant VIP_LIMIT = 25000 * 1e18; // 25,000 tokens + + // Track daily volumes and last transaction day + mapping(address => uint256) public userDailyVolume; + mapping(address => uint256) public lastTransactionDay; + + constructor(address _token0, address _token1) { + token0 = IERC20(_token0); + token1 = IERC20(_token1); + } + + function setUserFeeTier(address user, uint256 tier) external { + require(tier <= 2, "Invalid tier"); + userFeeTier[user] = tier; + } + + function getReserves() external view returns (uint256, uint256) { + return (reserve0, reserve1); + } + + function _mint(address _to, uint256 _amount) private { + balanceOf[_to] += _amount; + totalSupply += _amount; + } + + function _burn(address _from, uint256 _amount) private { + balanceOf[_from] -= _amount; + totalSupply -= _amount; + } + + function _update(uint256 _reserve0, uint256 _reserve1) private { + reserve0 = _reserve0; + reserve1 = _reserve1; + } + + function swap( + address _tokenIn, + uint256 _amountIn + ) external returns (uint256 amountOut) { + require( + _tokenIn == address(token0) || _tokenIn == address(token1), + "invalid token" + ); + require(_amountIn > 0, "amount in = 0"); + + _updateDailyVolume(msg.sender, _amountIn); + + bool isToken0 = _tokenIn == address(token0); + ( + IERC20 tokenIn, + IERC20 tokenOut, + uint256 reserveIn, + uint256 reserveOut + ) = isToken0 + ? (token0, token1, reserve0, reserve1) + : (token1, token0, reserve1, reserve0); + + tokenIn.transferFrom(msg.sender, address(this), _amountIn); + + /* How much dy for dx? xy = k @@ -67,23 +95,29 @@ contract CPAMM { (yx + ydx - xy) / (x + dx) = dy ydx / (x + dx) = dy */ - // 0.3% fee - uint256 amountInWithFee = (_amountIn * 997) / 1000; - amountOut = (reserveOut * amountInWithFee) / (reserveIn + amountInWithFee); + uint256 fee = getUserFee(msg.sender); + uint256 amountInWithFee = (_amountIn * (1000 - fee)) / 1000; - tokenOut.transfer(msg.sender, amountOut); + amountOut = + (reserveOut * amountInWithFee) / + (reserveIn + amountInWithFee); - _update(token0.balanceOf(address(this)), token1.balanceOf(address(this))); - } + tokenOut.transfer(msg.sender, amountOut); + + _update( + token0.balanceOf(address(this)), + token1.balanceOf(address(this)) + ); + } - function addLiquidity( - uint256 _amount0, - uint256 _amount1 - ) external returns (uint256 shares) { - token0.transferFrom(msg.sender, address(this), _amount0); - token1.transferFrom(msg.sender, address(this), _amount1); + function addLiquidity( + uint256 _amount0, + uint256 _amount1 + ) external onlyPremiumOrVip returns (uint256 shares) { + token0.transferFrom(msg.sender, address(this), _amount0); + token1.transferFrom(msg.sender, address(this), _amount1); - /* + /* How much dx, dy to add? xy = k @@ -98,15 +132,15 @@ contract CPAMM { x / y = dx / dy dy = y / x * dx */ - // Commented out because it's not needed for our PoC - // if (reserve0 > 0 || reserve1 > 0) { - // require( - // reserve0 * _amount1 == reserve1 * _amount0, - // "x / y != dx / dy" - // ); - // } - - /* + // Commented out because it's not needed for our PoC + // if (reserve0 > 0 || reserve1 > 0) { + // require( + // reserve0 * _amount1 == reserve1 * _amount0, + // "x / y != dx / dy" + // ); + // } + + /* How much shares to mint? f(x, y) = value of liquidity @@ -125,7 +159,7 @@ contract CPAMM { (L1 - L0) * T / L0 = s */ - /* + /* Claim (L1 - L0) / L0 = dx / x = dy / y @@ -156,23 +190,27 @@ contract CPAMM { Finally (L1 - L0) / L0 = dx / x = dy / y */ - if (totalSupply == 0) { - shares = Math.sqrt(_amount0 * _amount1); - } else { - shares = Math.min( - (_amount0 * totalSupply) / reserve0, (_amount1 * totalSupply) / reserve1 - ); + if (totalSupply == 0) { + shares = Math.sqrt(_amount0 * _amount1); + } else { + shares = Math.min( + (_amount0 * totalSupply) / reserve0, + (_amount1 * totalSupply) / reserve1 + ); + } + require(shares > 0, "shares = 0"); + _mint(msg.sender, shares); + + _update( + token0.balanceOf(address(this)), + token1.balanceOf(address(this)) + ); } - require(shares > 0, "shares = 0"); - _mint(msg.sender, shares); - _update(token0.balanceOf(address(this)), token1.balanceOf(address(this))); - } - - function removeLiquidity( - uint256 _shares - ) external returns (uint256 amount0, uint256 amount1) { - /* + function removeLiquidity( + uint256 _shares + ) external onlyPremiumOrVip returns (uint256 amount0, uint256 amount1) { + /* Claim dx, dy = amount of liquidity to remove dx = s / T * x @@ -206,19 +244,89 @@ contract CPAMM { dy = s / T * y */ - // bal0 >= reserve0 - // bal1 >= reserve1 - uint256 bal0 = token0.balanceOf(address(this)); - uint256 bal1 = token1.balanceOf(address(this)); + // bal0 >= reserve0 + // bal1 >= reserve1 + uint256 bal0 = token0.balanceOf(address(this)); + uint256 bal1 = token1.balanceOf(address(this)); + + amount0 = (_shares * bal0) / totalSupply; + amount1 = (_shares * bal1) / totalSupply; + require(amount0 > 0 && amount1 > 0, "amount0 or amount1 = 0"); + + _burn(msg.sender, _shares); + _update(bal0 - amount0, bal1 - amount1); + + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + } + + function getUserFee(address user) public view returns (uint256) { + if (userFeeTier[user] == 1) { + return PREMIUM_FEE; + } else if (userFeeTier[user] == 2) { + return VIP_FEE; + } + return BASIC_FEE; + } + + function _getUserLimit(address user) private view returns (uint256) { + if (userFeeTier[user] == 1) { + return PREMIUM_LIMIT; + } else if (userFeeTier[user] == 2) { + return VIP_LIMIT; + } + return BASIC_LIMIT; + } + + function _updateDailyVolume(address user, uint256 amount) private { + uint256 currentDay = block.timestamp / 86400; - amount0 = (_shares * bal0) / totalSupply; - amount1 = (_shares * bal1) / totalSupply; - require(amount0 > 0 && amount1 > 0, "amount0 or amount1 = 0"); + // Reset volume if it's a new day + if (currentDay > lastTransactionDay[user]) { + userDailyVolume[user] = 0; + lastTransactionDay[user] = currentDay; + } - _burn(msg.sender, _shares); - _update(bal0 - amount0, bal1 - amount1); + uint256 userLimit = _getUserLimit(user); + require( + userDailyVolume[user] + amount <= userLimit, + "Daily limit exceeded" + ); - token0.transfer(msg.sender, amount0); - token1.transfer(msg.sender, amount1); - } + userDailyVolume[user] += amount; + } + + function getRemainingDailyAllowance( + address user + ) external view returns (uint256) { + uint256 currentDay = block.timestamp / 86400; + + // If it's a new day, full allowance is available + if (currentDay > lastTransactionDay[user]) { + return _getUserLimit(user); + } + + uint256 userLimit = _getUserLimit(user); + if (userDailyVolume[user] >= userLimit) { + return 0; + } + + return userLimit - userDailyVolume[user]; + } + + modifier onlyPremiumOrVip() { + require( + userFeeTier[msg.sender] == 2 || userFeeTier[msg.sender] == 1, + "Only VIP or premium users can call this function" + ); + _; + } + + modifier onlyVip() { + require( + userFeeTier[msg.sender] == 2, + "Only VIP users can call this function" + ); + _; + } } diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 587bd08..5fc693a 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -2,22 +2,20 @@ import "@rainbow-me/rainbowkit/styles.css"; import { GeistMono } from "geist/font/mono"; import { GeistSans } from "geist/font/sans"; import { Metadata } from "next"; -import { ScaffoldEthAppWithProviders } from "~~/components/ScaffoldEthAppWithProviders"; +import { AppWithProviders } from "~~/components/AppWithProviders"; import { ThemeProvider } from "~~/components/ThemeProvider"; import "~~/styles/globals.css"; export const metadata: Metadata = { title: "Double Zero Swap", description: "Double Zero Swap" }; -const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => { +export default function Layout({ children }: { children: React.ReactNode }) { return ( - {children} + {children} ); -}; - -export default ScaffoldEthApp; +} diff --git a/web/app/page.tsx b/web/app/page.tsx index 341dce0..ea74c14 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -39,7 +39,7 @@ export default function UniswapClone() { const dai = useDaiToken(); const wbtc = useWbtcToken(); const { writeContractAsync } = useWriteContract(); - const { daiPoolLiquidity, wbtcPoolLiquidity } = useCpamm(); + const { daiPoolLiquidity, wbtcPoolLiquidity, fee, remainingDailyAllowance, refetchAll: cpammRefetchAll } = useCpamm(); const { value: isSwapping, setValue: setIsSwapping } = useBoolean(false); const [swapState, setSwapState] = useState({ from: { @@ -240,6 +240,7 @@ export default function UniswapClone() { setIsSwapping(false); dai.refetchAll(); wbtc.refetchAll(); + cpammRefetchAll(); } }; @@ -298,8 +299,24 @@ export default function UniswapClone() { wbtcPoolLiquidity={wbtcPoolLiquidity} /> -
- 1 {swapState.from.token.symbol} = {formatTokenWithDecimals(swapPrice, 18)} {swapState.to.token.symbol} +
+
+ Price: + + 1 {swapState.from.token.symbol} = {formatTokenWithDecimals(swapPrice, 18)}{" "} + {swapState.to.token.symbol} + +
+
+ Fee: + {fee ? `${Number(fee) / 10}%` : "-"} +
+
+ Daily Limit: + + {remainingDailyAllowance ? formatTokenWithDecimals(remainingDailyAllowance, 18) : "-"} tokens + +