diff --git a/src/PartyPool.sol b/src/PartyPool.sol index dcf8aa0..fc2e0b1 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -58,7 +58,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool function fees() external view returns (uint256[] memory) { return _fees; } /// @notice Effective combined fee in ppm for (i as input, j as output) - function fee(uint256 i, uint256 j) external view returns (uint256) { return _pairFeePpm(i,j); } + function fee(uint256 i, uint256 j) external view returns (uint256) { return _pairFeePpmView(i,j); } /// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts. uint256 private immutable FLASH_FEE_PPM; @@ -240,7 +240,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool uint256 maxAmountIn, int128 limitPrice ) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee) { - (uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice); + (uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, _pairFeePpmView(inputTokenIndex, outputTokenIndex)); return (grossIn, outUint, feeUint); } @@ -259,7 +259,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool // Compute amounts using the same path as views (uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) = - _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice); + _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, _pairFeePpm(inputTokenIndex, outputTokenIndex)); // Cache token references for fewer SLOADs IERC20 tokenIn = _tokens[inputTokenIndex]; @@ -309,7 +309,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool uint256 inputTokenIndex, uint256 outputTokenIndex, uint256 maxAmountIn, - int128 limitPrice + int128 limitPrice, + uint256 feePpm ) internal view returns ( uint256 grossIn, @@ -321,8 +322,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool ) { // Estimate max net input (fee on gross rounded up, then subtract) - uint256 pairFeePpm = _pairFeePpm(inputTokenIndex, outputTokenIndex); - (, uint256 netUintForSwap) = _computeFee(maxAmountIn, pairFeePpm); + (, uint256 netUintForSwap) = _computeFee(maxAmountIn, feePpm); // Convert to internal (floor) int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, _bases[inputTokenIndex]); @@ -338,8 +338,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool // Compute gross transfer including fee on the used input (ceil) feeUint = 0; grossIn = amountInUintNoFee; - if (pairFeePpm > 0) { - feeUint = _ceilFee(amountInUintNoFee, pairFeePpm); + if (feePpm > 0) { + feeUint = _ceilFee(amountInUintNoFee, feePpm); grossIn += feeUint; } diff --git a/src/PartyPoolBase.sol b/src/PartyPoolBase.sol index aa3a20a..8e8ef3f 100644 --- a/src/PartyPoolBase.sol +++ b/src/PartyPoolBase.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; -import "./NativeWrapper.sol"; import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; -import {ERC20Internal} from "./ERC20Internal.sol"; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {LMSRStabilized} from "./LMSRStabilized.sol"; -import {PartyPoolHelpers} from "./PartyPoolHelpers.sol"; -import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; +import {ERC20Internal} from "./ERC20Internal.sol"; +import {LMSRStabilized} from "./LMSRStabilized.sol"; +import {NativeWrapper} from "./NativeWrapper.sol"; import {OwnableInternal} from "./OwnableInternal.sol"; +import {PartyPoolHelpers} from "./PartyPoolHelpers.sol"; /// @notice Abstract base contract that contains storage and internal helpers only. /// No external/public functions here. @@ -26,6 +26,7 @@ abstract contract PartyPoolBase is OwnableInternal, ERC20Internal, ReentrancyGua /// @notice Per-asset swap fees in ppm. Fees are applied on input for swaps; see helpers for composition rules. uint256[] internal _fees; + mapping( uint256 => uint256 ) internal _pairFees; // // Internal state @@ -92,12 +93,22 @@ abstract contract PartyPoolBase is OwnableInternal, ERC20Internal, ReentrancyGua // We implement this as: ceil( fi + fj - (fi*fj)/1e6 ) for the real-valued expression. // For integer arithmetic with fi,fj in ppm this is equal to: fi + fj - floor( (fi*fj)/1e6 ). // So we compute prod = fi * fj, prodDiv = prod / 1e6 (floor), and return fi + fj - prodDiv. - function _pairFeePpm(uint256 i, uint256 j) internal view returns (uint256) { + function _pairFeePpmView(uint256 i, uint256 j) internal view returns (uint256) { uint256 fi = _fees[i]; uint256 fj = _fees[j]; return fi + fj - fi * fj / 1_000_000; } + function _pairFeePpm(uint256 i, uint256 j) internal returns (uint256 fee) { + uint256 key = 1000 * i + j; + fee = _pairFees[key]; + if (fee == 0) { + // store fee in cache + fee = _pairFeePpmView(i,j); + _pairFees[key] = fee; + } + } + // Convert uint token amount -> internal 64.64 (floor). Uses ABDKMath64x64.divu which truncates. function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) { // internal = amount / base (as Q64.64) diff --git a/test/GasTest.sol b/test/GasTest.sol index 722c90f..ef75f4c 100644 --- a/test/GasTest.sol +++ b/test/GasTest.sol @@ -237,10 +237,10 @@ contract GasTest is Test { vm.prank(alice); TestERC20(address(tokens[1])).approve(address(testPool), type(uint256).max); - uint256 maxIn = 1_000; + uint256 maxIn = 10_000; - // Perform 10 swaps alternating directions to avoid large imbalance - for (uint256 i = 0; i < 10; i++) { + // Perform swaps alternating directions to avoid large imbalance + for (uint256 i = 0; i < 20; i++) { vm.prank(alice); if (i % 2 == 0) { // swap token0 -> token1 @@ -249,6 +249,9 @@ contract GasTest is Test { // swap token1 -> token0 testPool.swap(alice, alice, 1, 0, maxIn, 0, 0, false); } + // shake up the bits + maxIn *= 787; + maxIn /= 1000; } }