diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6cd4f81 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Adekunle Bamz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/StablePool.sol b/src/StablePool.sol index 0a285c0..2ee0599 100644 --- a/src/StablePool.sol +++ b/src/StablePool.sol @@ -348,6 +348,258 @@ contract StablePool is IStablePool { RECEIVE ETH //////////////////////////////////////////////////////////////*/ + /*////////////////////////////////////////////////////////////// + BULK OPERATIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Struct for bulk swap parameters + struct BulkSwap { + address tokenIn; + uint256 amountIn; + uint256 minAmountOut; + } + + /// @notice Struct for bulk liquidity addition parameters + struct BulkLiquidityAddition { + uint256 amountUSDC; + uint256 amountUSDT; + uint256 minLpTokens; + } + + /// @notice Struct for bulk liquidity removal parameters + struct BulkLiquidityRemoval { + uint256 lpTokens; + uint256 minUSDC; + uint256 minUSDT; + } + + /// @notice Event emitted when bulk swaps are executed + event BulkSwapsExecuted( + address indexed caller, + uint256 totalSwaps, + uint256 totalValueTransferred + ); + + /// @notice Event emitted when bulk liquidity is added + event BulkLiquidityAdded( + address indexed provider, + uint256 totalAdditions, + uint256 totalLpTokens + ); + + /// @notice Event emitted when bulk liquidity is removed + event BulkLiquidityRemoved( + address indexed provider, + uint256 totalRemovals, + uint256 totalLpBurned, + uint256 totalUSDC, + uint256 totalUSDT + ); + + /// @notice Execute multiple swaps in a single transaction + /// @param swaps Array of swap parameters (up to 10 swaps per transaction) + /// @return totalAmountOut Total output amount across all swaps + function bulkSwap(BulkSwap[] calldata swaps) + external + payable + returns (uint256 totalAmountOut) + { + uint256 totalSwaps = swaps.length; + require(totalSwaps > 0 && totalSwaps <= 10, "StablePool: invalid swap count"); + + uint256 totalFeesRequired = totalSwaps * SWAP_FEE; + require(msg.value == totalFeesRequired, "StablePool: incorrect total fee"); + + uint256 totalValueTransferred = 0; + + for (uint256 i = 0; i < totalSwaps; i++) { + BulkSwap calldata swapData = swaps[i]; + require(swapData.amountIn > 0, "StablePool: zero amount"); + + // Validate token and determine direction + bool isUsdcIn = swapData.tokenIn == address(usdc); + require(isUsdcIn || swapData.tokenIn == address(usdt), "StablePool: invalid token"); + + // Calculate output using stable-swap math + uint256 amountOut = _calculateSwapOutput(swapData.amountIn, isUsdcIn); + require(amountOut >= swapData.minAmountOut, "StablePool: slippage"); + + // Check sufficient output reserves + if (isUsdcIn) { + require(amountOut <= reserveUSDT, "StablePool: insufficient USDT"); + } else { + require(amountOut <= reserveUSDC, "StablePool: insufficient USDC"); + } + + // Transfer input token in + IERC20 inputToken = isUsdcIn ? usdc : usdt; + _safeTransferFrom(inputToken, msg.sender, address(this), swapData.amountIn); + + // Update reserves + if (isUsdcIn) { + reserveUSDC += swapData.amountIn; + reserveUSDT -= amountOut; + } else { + reserveUSDT += swapData.amountIn; + reserveUSDC -= amountOut; + } + + // Transfer output token out + IERC20 outputToken = isUsdcIn ? usdt : usdc; + _safeTransfer(outputToken, msg.sender, amountOut); + + // Accumulate totals + totalAmountOut += amountOut; + totalValueTransferred += swapData.amountIn; + } + + // Send total fees to recipient + (bool success, ) = feeRecipient.call{value: totalFeesRequired}(""); + require(success, "StablePool: fee transfer failed"); + + emit BulkSwapsExecuted(msg.sender, totalSwaps, totalValueTransferred); + + return totalAmountOut; + } + + /// @notice Execute bulk liquidity additions + /// @param additions Array of liquidity addition parameters (up to 5 additions) + /// @return totalLpTokens Total LP tokens minted across all additions + function bulkAddLiquidity(BulkLiquidityAddition[] calldata additions) + external + returns (uint256 totalLpTokens) + { + uint256 totalAdditions = additions.length; + require(totalAdditions > 0 && totalAdditions <= 5, "StablePool: invalid addition count"); + + for (uint256 i = 0; i < totalAdditions; i++) { + BulkLiquidityAddition calldata addition = additions[i]; + require(addition.amountUSDC > 0 || addition.amountUSDT > 0, "StablePool: zero amounts"); + + uint256 totalSupply = lpToken.totalSupply(); + uint256 lpTokensMinted; + + if (totalSupply == 0) { + // First deposit: LP tokens = sum of normalized amounts + lpTokensMinted = (addition.amountUSDC * usdcMultiplier) + (addition.amountUSDT * usdtMultiplier); + require(lpTokensMinted > 0, "StablePool: insufficient initial liquidity"); + } else { + // Subsequent deposits: proportional to existing liquidity + uint256 totalReserves = (reserveUSDC * usdcMultiplier) + (reserveUSDT * usdtMultiplier); + uint256 depositValue = (addition.amountUSDC * usdcMultiplier) + (addition.amountUSDT * usdtMultiplier); + lpTokensMinted = (depositValue * totalSupply) / totalReserves; + } + + require(lpTokensMinted >= addition.minLpTokens, "StablePool: slippage"); + + // Transfer tokens in + if (addition.amountUSDC > 0) { + _safeTransferFrom(usdc, msg.sender, address(this), addition.amountUSDC); + reserveUSDC += addition.amountUSDC; + } + if (addition.amountUSDT > 0) { + _safeTransferFrom(usdt, msg.sender, address(this), addition.amountUSDT); + reserveUSDT += addition.amountUSDT; + } + + // Mint LP tokens + lpToken.mint(msg.sender, lpTokensMinted); + + totalLpTokens += lpTokensMinted; + } + + emit BulkLiquidityAdded(msg.sender, totalAdditions, totalLpTokens); + } + + /// @notice Execute bulk liquidity removals + /// @param removals Array of liquidity removal parameters (up to 5 removals) + /// @return totalUSDC Total USDC withdrawn across all removals + /// @return totalUSDT Total USDT withdrawn across all removals + function bulkRemoveLiquidity(BulkLiquidityRemoval[] calldata removals) + external + returns (uint256 totalUSDC, uint256 totalUSDT) + { + uint256 totalRemovals = removals.length; + require(totalRemovals > 0 && totalRemovals <= 5, "StablePool: invalid removal count"); + + uint256 totalLpBurned = 0; + + for (uint256 i = 0; i < totalRemovals; i++) { + BulkLiquidityRemoval calldata removal = removals[i]; + require(removal.lpTokens > 0, "StablePool: zero LP tokens"); + + uint256 totalSupply = lpToken.totalSupply(); + require(totalSupply > 0, "StablePool: no liquidity"); + + // Calculate proportional amounts + uint256 amountUSDC = (removal.lpTokens * reserveUSDC) / totalSupply; + uint256 amountUSDT = (removal.lpTokens * reserveUSDT) / totalSupply; + + require(amountUSDC >= removal.minUSDC, "StablePool: USDC slippage"); + require(amountUSDT >= removal.minUSDT, "StablePool: USDT slippage"); + + // Burn LP tokens first (checks balance) + lpToken.burn(msg.sender, removal.lpTokens); + + // Update reserves + reserveUSDC -= amountUSDC; + reserveUSDT -= amountUSDT; + + // Transfer tokens out + if (amountUSDC > 0) { + _safeTransfer(usdc, msg.sender, amountUSDC); + } + if (amountUSDT > 0) { + _safeTransfer(usdt, msg.sender, amountUSDT); + } + + // Accumulate totals + totalUSDC += amountUSDC; + totalUSDT += amountUSDT; + totalLpBurned += removal.lpTokens; + } + + emit BulkLiquidityRemoved(msg.sender, totalRemovals, totalLpBurned, totalUSDC, totalUSDT); + } + + /// @notice Get bulk operation limits + /// @return maxBulkSwaps Maximum swaps per bulk transaction + /// @return maxBulkLiquidity Maximum liquidity operations per bulk transaction + function getBulkLimits() + external + pure + returns (uint256 maxBulkSwaps, uint256 maxBulkLiquidity) + { + return (10, 5); + } + + /// @notice Estimate gas for bulk operations + /// @param operationCount Number of operations + /// @param isSwap True if estimating swaps, false for liquidity operations + /// @return estimatedGas Approximate gas cost + function estimateBulkGas(uint256 operationCount, bool isSwap) + external + pure + returns (uint256 estimatedGas) + { + require(operationCount > 0, "StablePool: invalid count"); + + uint256 baseGas = 21000; // Base transaction gas + uint256 perOperationGas; + + if (isSwap) { + // Swap operations + perOperationGas = operationCount <= 10 ? 85000 : 100000; + require(operationCount <= 10, "StablePool: too many swaps"); + } else { + // Liquidity operations + perOperationGas = 130000; + require(operationCount <= 5, "StablePool: too many operations"); + } + + return baseGas + (perOperationGas * operationCount); + } + /// @notice Reject direct ETH transfers (fees must go through swap) receive() external payable { revert("StablePool: use swap()");