Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
252 changes: 252 additions & 0 deletions src/StablePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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()");
Expand Down