prefunding

This commit is contained in:
tim
2025-11-11 18:33:14 -04:00
parent ddfe7651ea
commit 70960630ce
8 changed files with 722 additions and 69 deletions

View File

@@ -1,19 +1,22 @@
#!/bin/bash
report() {
local name=${2:-$1}
REPORT=$(forge test --mc GasTest --mt "$1" --gas-report)
SWAP=$(echo "$REPORT" | grep 'swap ' | cut -d '|' -f 5 | xargs)
MINT=$(echo "$REPORT" | grep 'mint ' | cut -d '|' -f 5 | xargs)
SWAPMINT=$(echo "$REPORT" | grep 'swapMint ' | cut -d '|' -f 5 | xargs)
printf "%-10s %10s %10s %10s\n" "$1" "$SWAP" "$MINT" "$SWAPMINT"
printf "%-15s %10s %10s %10s\n" "$name" "$SWAP" "$MINT" "$SWAPMINT"
}
# Print header
printf "%-10s %10s %10s %10s\n" "" "swap" "mint" "swapMint"
printf "%-15s %10s %10s %10s\n" "" "swap" "mint" "swapMint"
printf "%s\n" " ------ --------- ---------"
report GasPair
report StablePair
report GasPair Pair
report StablePair "Stable Pair"
report Ten
report Callback "Ten-callback"
report Prefunding "Ten-prefunding"
report Twenty
report Fifty

11
src/Funding.sol Normal file
View File

@@ -0,0 +1,11 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
library Funding {
/// @notice a constant passed to swap as the fundingSelector to indicate that the payer has used regular ERC20 approvals to allow the pool to move the necessary input tokens.
bytes4 internal constant USE_APPROVALS = 0x00000000;
/// @notice a constant passed to swap as the fundingSelector to indicate that the payer has already sent sufficient input tokens to the pool before calling swap, so no movement of input tokens is required.
bytes4 internal constant USE_PREFUNDING = 0x00000001;
}

View File

@@ -1,12 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
import "./IOwnable.sol";
import "./LMSRStabilized.sol";
import "./NativeWrapper.sol";
import {IERC20Metadata} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {IERC3156FlashBorrower} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {IERC20Metadata} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {IOwnable} from "./IOwnable.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol";
import {NativeWrapper} from "./NativeWrapper.sol";
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
@@ -178,7 +178,7 @@ interface IPartyPool is IERC20Metadata, IOwnable {
/// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver.
/// Non-standard tokens (fee-on-transfer, rebasers) are rejected via balance checks.
/// @param payer address of the account that pays for the swap
/// @param selector If zero, then regular ERC20 approvals must be given by the payere to the pool to move the required input amount. If this selector is nonzero, then a callback style funding mechanism is used where the given selector is invoked on the payer, passing the arguments of (address inputToken, uint256 inputAmount). The callback function must send the given amount of input coin to the pool in ordr to continue the swap transaction, otherwise "Insufficient funds" is thrown.
/// @param fundingSelector If set to USE_APPROVALS, then the payer must use regular ERC20 approvals to authorize the pool to move the required input amount. If this fundingSelector is USE_PREFUNDING, then all of the input amount is expected to have already been sent to the pool and no additional transfers are needed. Refunds of excess input amount are NOT provided and it is illegal to use this funding method with a limit price. Otherwise, for any other fundingSelector value, a callback style funding mechanism is used where the given selector is invoked on the payer, passing the arguments of (address inputToken, uint256 inputAmount). The callback function must send the given amount of input coin to the pool in order to continue the swap transaction, otherwise "Insufficient funds" is thrown.
/// @param receiver address that will receive the output tokens
/// @param inputTokenIndex index of input asset
/// @param outputTokenIndex index of output asset
@@ -188,7 +188,7 @@ interface IPartyPool is IERC20Metadata, IOwnable {
/// @return amountIn actual input used (uint256), amountOut actual output sent (uint256), inFee fee taken from the input (uint256)
function swap(
address payer,
bytes4 selector,
bytes4 fundingSelector,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,

View File

@@ -1,25 +1,22 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import {Address} from "../lib/openzeppelin-contracts/contracts/utils/Address.sol";
import {ERC20External} from "./ERC20External.sol";
import {IERC3156FlashBorrower} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {IPartyFlashCallback} from "./IPartyFlashCallback.sol";
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {Address} from "../lib/openzeppelin-contracts/contracts/utils/Address.sol";
import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
import {ERC20External} from "./ERC20External.sol";
import {Funding} from "./Funding.sol";
import {IPartyPool} from "./IPartyPool.sol";
import {LMSRStabilizedBalancedPair} from "./LMSRStabilizedBalancedPair.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol";
import {NativeWrapper} from "./NativeWrapper.sol";
import {OwnableExternal} from "./OwnableExternal.sol";
import {OwnableInternal} from "./OwnableInternal.sol";
import {PartyPoolBase} from "./PartyPoolBase.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
import {Proxy} from "../lib/openzeppelin-contracts/contracts/proxy/Proxy.sol";
import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC3156FlashLender} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashLender.sol";
import {NativeWrapper} from "./NativeWrapper.sol";
import {OwnableExternal} from "./OwnableExternal.sol";
import {IPartyInfo} from "./IPartyInfo.sol";
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
@@ -248,7 +245,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
/// @inheritdoc IPartyPool
function swap(
address payer,
bytes4 selector,
bytes4 fundingSelector,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
@@ -267,13 +264,19 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
IERC20 tokenIn = _tokens[inputTokenIndex];
IERC20 tokenOut = _tokens[outputTokenIndex];
if ( selector == bytes4(0) )
if (fundingSelector == Funding.USE_APPROVALS)
// Regular ERC20 permit of the pool to move the tokens
_receiveTokenFrom(payer, tokenIn, totalTransferAmount);
else if (fundingSelector == Funding.USE_PREFUNDING) {
require(limitPrice==0, 'Prefunding cannot be used with a limit price');
uint256 balance = tokenIn.balanceOf(address(this));
uint256 prevBalance = _cachedUintBalances[inputTokenIndex] + _protocolFeesOwed[inputTokenIndex];
require( balance - prevBalance == totalTransferAmount, 'Incorrect prefunding amount');
}
else {
// Callback-style funding mechanism
uint256 startingBalance = tokenIn.balanceOf(address(this));
bytes memory data = abi.encodeWithSelector(selector, tokenIn, totalTransferAmount);
bytes memory data = abi.encodeWithSelector(fundingSelector, tokenIn, totalTransferAmount);
// Invoke the payer callback; no return value expected (reverts on failure)
Address.functionCall(payer, data);
uint256 endingBalance = tokenIn.balanceOf(address(this));

610
test/FundingSwapTest.sol Normal file
View File

@@ -0,0 +1,610 @@
// SPDX-License-Identifier: UNLICENSED
/* solhint-disable */
pragma solidity ^0.8.30;
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import {Test} from "../lib/forge-std/src/Test.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {Funding} from "../src/Funding.sol";
import {LMSRStabilized} from "../src/LMSRStabilized.sol";
import {PartyInfo} from "../src/PartyInfo.sol";
import {PartyPool} from "../src/PartyPool.sol";
import {Deploy} from "./Deploy.sol";
/// @notice Minimal ERC20 token for tests with an external mint function.
contract TestERC20 is ERC20 {
constructor(string memory name_, string memory symbol_, uint256 initialSupply) ERC20(name_, symbol_) {
if (initialSupply > 0) {
_mint(msg.sender, initialSupply);
}
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function approveMax(address spender) external {
_approve(msg.sender, spender, type(uint256).max);
}
}
/// @notice Test contract that provides token funds when called by PartyPool.swap via a selector.
/// The pool will call the payer with (token, amount) and expects the payer to transfer the input token
/// into the pool. This contract implements that provider function.
contract SwapCallbackContract {
address public pool;
address public tokenSource;
bool public shouldFail;
constructor(address _pool) {
pool = _pool;
}
function setTokenSource(address _tokenSource) external {
tokenSource = _tokenSource;
}
function setShouldFail(bool _shouldFail) external {
shouldFail = _shouldFail;
}
/// @notice Called by PartyPool.swap on the payer. Signature must be:
/// provideFunding(address token, uint256 amount)
/// @dev The pool will call this function to request the input token; this function
/// pulls funds from tokenSource (via ERC20.transferFrom) into the pool.
function provideFunding(address token, uint256 amount) external {
require(msg.sender == pool, "Callback not called by pool");
if (shouldFail) revert("callback failed");
require(tokenSource != address(0), "no token source");
// Pull the required tokens from tokenSource into the pool
TestERC20(token).transferFrom(tokenSource, pool, amount);
}
}
/// @notice Tests for PartyPool swap functionality using alternative funding mechanisms:
/// pre-funding and callback method. Validates that input/output amounts match swap results
/// view calls and that pool balances are correct.
contract FundingTest is Test {
using ABDKMath64x64 for int128;
TestERC20 token0;
TestERC20 token1;
TestERC20 token2;
PartyPool pool;
PartyPool poolZeroFee;
PartyInfo info;
SwapCallbackContract callbackContract;
address alice;
address bob;
// Common parameters
int128 tradeFrac;
int128 targetSlippage;
uint256 constant INIT_BAL = 1_000_000; // initial token units for each token
// Callback funding selector - the pool will call payer.provideFunding(address token, uint256 amount)
bytes4 constant CALLBACK = SwapCallbackContract.provideFunding.selector;
function setUp() public {
alice = address(0xA11ce);
bob = address(0xB0b);
// Deploy three ERC20 test tokens
token0 = new TestERC20("T0", "T0", 0);
token1 = new TestERC20("T1", "T1", 0);
token2 = new TestERC20("T2", "T2", 0);
// Mint initial balances to the test contract
token0.mint(address(this), INIT_BAL);
token1.mint(address(this), INIT_BAL);
token2.mint(address(this), INIT_BAL);
// Configure LMSR parameters
tradeFrac = ABDKMath64x64.divu(100, 10_000); // 0.01
targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001
// Build arrays for pool constructor
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(token2));
// Deploy pool with a small fee (0.1%)
uint256 feePpm = 1000;
int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
pool = Deploy.newPartyPool(address(this), "LP", "LP", tokens, kappa, feePpm, feePpm, false);
// Transfer initial deposit amounts into pool
token0.transfer(address(pool), INIT_BAL);
token1.transfer(address(pool), INIT_BAL);
token2.transfer(address(pool), INIT_BAL);
// Perform initial mint
pool.initialMint(address(this), INIT_BAL * tokens.length * 10**18);
// Deploy pool with zero fees for exact balance matching
poolZeroFee = Deploy.newPartyPool(address(this), "LP_ZERO", "LP_ZERO", tokens, kappa, 0, 0, false);
// Mint additional tokens for zero-fee pool
token0.mint(address(this), INIT_BAL);
token1.mint(address(this), INIT_BAL);
token2.mint(address(this), INIT_BAL);
// Transfer to zero-fee pool
token0.transfer(address(poolZeroFee), INIT_BAL);
token1.transfer(address(poolZeroFee), INIT_BAL);
token2.transfer(address(poolZeroFee), INIT_BAL);
// Initialize zero-fee pool
poolZeroFee.initialMint(address(this), INIT_BAL * tokens.length * 10**18);
// Mint tokens to alice and bob for testing
token0.mint(alice, INIT_BAL);
token1.mint(alice, INIT_BAL);
token2.mint(alice, INIT_BAL);
token0.mint(bob, INIT_BAL);
token1.mint(bob, INIT_BAL);
token2.mint(bob, INIT_BAL);
// Deploy callback contract
callbackContract = new SwapCallbackContract(address(pool));
info = Deploy.newInfo();
}
/* ----------------------
Pre-funding Tests
---------------------- */
/// @notice Test swap using pre-funding mechanism with regular fee pool
function testSwapWithPreFunding() public {
uint256 maxIn = 10_000;
// Pre-fund the pool by transferring tokens before the swap
vm.startPrank(alice);
token0.transfer(address(pool), maxIn);
uint256 poolToken0Before = token0.balanceOf(address(pool));
uint256 poolToken1Before = token1.balanceOf(address(pool));
uint256 bobToken1Before = token1.balanceOf(bob);
// Execute swap using Funding.USE_PREFUNDING for pre-funded: token0 -> token1
(uint256 amountIn, uint256 amountOut, uint256 fee) = pool.swap(
alice, // payer (not used with pre-funded)
Funding.USE_PREFUNDING,
bob, // receiver
0, // inputTokenIndex (token0)
1, // outputTokenIndex (token1)
maxIn, // maxAmountIn
0, // limitPrice
0, // deadline
false // unwrap
);
// Verify amounts
assertTrue(amountIn > 0, "expected some input used");
assertTrue(amountOut > 0, "expected some output returned");
assertTrue(amountIn <= maxIn, "used input must not exceed max");
assertTrue(fee <= amountIn, "fee must not exceed total input");
// Bob received the output
assertEq(token1.balanceOf(bob), bobToken1Before + amountOut, "Bob should receive output");
// Pool balances changed as expected
// Input token increased (tokens were already transferred)
assertEq(token0.balanceOf(address(pool)), poolToken0Before, "Pool token0 should remain at pre-funded level");
// Output token decreased
assertEq(token1.balanceOf(address(pool)), poolToken1Before - amountOut, "Pool token1 should decrease");
// If any unused tokens, they remain in the pool
uint256 unusedTokens = maxIn - amountIn;
if (unusedTokens > 0) {
// The pre-funded amount stays in the pool
assertTrue(token0.balanceOf(address(pool)) >= poolToken0Before - amountIn, "Unused tokens remain");
}
vm.stopPrank();
}
/// @notice Test swap using pre-funding with zero-fee pool to verify exact pool balance matching
function testSwapWithPreFundingZeroFeeExactBalances() public {
uint256 maxIn = 10_000;
// Pre-fund the pool
vm.startPrank(alice);
token0.transfer(address(poolZeroFee), maxIn);
uint256 poolToken0Before = token0.balanceOf(address(poolZeroFee));
uint256 poolToken1Before = token1.balanceOf(address(poolZeroFee));
// Execute swap
(uint256 amountIn, uint256 amountOut, uint256 fee) = poolZeroFee.swap(
alice,
Funding.USE_PREFUNDING,
bob,
0, // token0 -> token1
1,
maxIn,
0,
0,
false
);
// With zero fees, fee should be 0
assertEq(fee, 0, "Fee should be zero in zero-fee pool");
// Pool balances should match exactly (no rounding errors with zero fees)
uint256 poolToken0After = token0.balanceOf(address(poolZeroFee));
uint256 poolToken1After = token1.balanceOf(address(poolZeroFee));
// Net change: input increased by amountIn (already pre-funded), output decreased by amountOut
assertEq(poolToken0After, poolToken0Before, "Pool token0 balance exact");
assertEq(poolToken1After, poolToken1Before - amountOut, "Pool token1 balance exact");
vm.stopPrank();
}
/// @notice Test that pre-funding with insufficient tokens reverts appropriately
function testSwapWithPreFundingInsufficientTokensReverts() public {
uint256 maxIn = 10_000;
uint256 insufficientAmount = maxIn / 2; // Only half of what's needed
vm.startPrank(alice);
token0.transfer(address(pool), insufficientAmount);
// This should revert because the pool doesn't have enough pre-funded tokens
vm.expectRevert();
pool.swap(
alice,
Funding.USE_PREFUNDING,
bob,
0,
1,
maxIn,
0,
0,
false
);
vm.stopPrank();
}
/* ----------------------
Callback Method Tests
---------------------- */
/// @notice Test swap using callback mechanism
function testSwapWithCallback() public {
uint256 maxIn = 10_000;
// Setup callback contract to use alice's tokens
callbackContract.setTokenSource(alice);
callbackContract.setShouldFail(false);
vm.startPrank(alice);
// Alice approves callback contract to transfer tokens
token0.approve(address(callbackContract), type(uint256).max);
uint256 aliceToken0Before = token0.balanceOf(alice);
uint256 poolToken0Before = token0.balanceOf(address(pool));
uint256 poolToken1Before = token1.balanceOf(address(pool));
uint256 bobToken1Before = token1.balanceOf(bob);
// Execute swap using callback: token0 -> token1
// The payer address (callbackContract) will receive the callback
vm.stopPrank();
(uint256 amountIn, uint256 amountOut, uint256 fee) = pool.swap(
address(callbackContract), // payer (receives callback)
CALLBACK,
bob, // receiver
0, // inputTokenIndex (token0)
1, // outputTokenIndex (token1)
maxIn, // maxAmountIn
0, // limitPrice
0, // deadline
false // unwrap
);
// Verify amounts
assertTrue(amountIn > 0, "expected some input used");
assertTrue(amountOut > 0, "expected some output returned");
assertTrue(amountIn <= maxIn, "used input must not exceed max");
assertTrue(fee <= amountIn, "fee must not exceed total input");
// Alice's tokens were used (via callback)
assertEq(token0.balanceOf(alice), aliceToken0Before - amountIn, "Alice tokens should decrease");
// Bob received the output
assertEq(token1.balanceOf(bob), bobToken1Before + amountOut, "Bob should receive output");
// Pool balances changed as expected
assertEq(token0.balanceOf(address(pool)), poolToken0Before + amountIn, "Pool token0 should increase");
assertEq(token1.balanceOf(address(pool)), poolToken1Before - amountOut, "Pool token1 should decrease");
}
/// @notice Test swap callback with zero-fee pool for exact balance matching
function testSwapWithCallbackZeroFeeExactBalances() public {
uint256 maxIn = 10_000;
// Setup callback for zero-fee pool
SwapCallbackContract zeroFeeCallback = new SwapCallbackContract(address(poolZeroFee));
zeroFeeCallback.setTokenSource(alice);
zeroFeeCallback.setShouldFail(false);
vm.startPrank(alice);
token0.approve(address(zeroFeeCallback), type(uint256).max);
vm.stopPrank();
uint256 poolToken0Before = token0.balanceOf(address(poolZeroFee));
uint256 poolToken1Before = token1.balanceOf(address(poolZeroFee));
// Execute swap
(uint256 amountIn, uint256 amountOut, uint256 fee) = poolZeroFee.swap(
address(zeroFeeCallback),
CALLBACK,
bob,
0,
1,
maxIn,
0,
0,
false
);
// With zero fees, fee should be 0
assertEq(fee, 0, "Fee should be zero in zero-fee pool");
// Pool balances should match exactly
uint256 poolToken0After = token0.balanceOf(address(poolZeroFee));
uint256 poolToken1After = token1.balanceOf(address(poolZeroFee));
assertEq(poolToken0After, poolToken0Before + amountIn, "Pool token0 balance exact");
assertEq(poolToken1After, poolToken1Before - amountOut, "Pool token1 balance exact");
}
/// @notice Test that callback failure causes swap to revert
function testSwapWithCallbackFailureReverts() public {
uint256 maxIn = 10_000;
callbackContract.setTokenSource(alice);
callbackContract.setShouldFail(true); // Make callback fail
vm.expectRevert();
pool.swap(
address(callbackContract),
CALLBACK,
bob,
0,
1,
maxIn,
0,
0,
false
);
}
/* ----------------------
Validation Against swapAmounts()
---------------------- */
/// @notice Verify that pre-funded swap amounts match swapAmounts() view predictions
function testPreFundingMatchesSwapAmountsView() public {
uint256 maxIn = 10_000;
// Perform a reference swap with USE_APPROVALS to get expected amounts
vm.startPrank(bob);
token0.approve(address(pool), type(uint256).max);
(uint256 refAmountIn, uint256 refAmountOut, uint256 refFee) = pool.swap(
bob,
Funding.USE_APPROVALS,
bob,
0,
1,
maxIn,
0,
0,
false
);
vm.stopPrank();
// Reset pool state by creating a fresh pool with identical parameters
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(token2));
uint256 feePpm = 1000;
int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool testPool = Deploy.newPartyPool(address(this), "LP_TEST", "LP_TEST", tokens, kappa, feePpm, feePpm, false);
token0.mint(address(this), INIT_BAL);
token1.mint(address(this), INIT_BAL);
token2.mint(address(this), INIT_BAL);
token0.transfer(address(testPool), INIT_BAL);
token1.transfer(address(testPool), INIT_BAL);
token2.transfer(address(testPool), INIT_BAL);
testPool.initialMint(address(this), INIT_BAL * tokens.length * 10**18);
// Now test pre-funding with same initial state
vm.startPrank(alice);
token0.transfer(address(testPool), maxIn);
(uint256 preAmountIn, uint256 preAmountOut, uint256 preFee) = testPool.swap(
alice,
Funding.USE_PREFUNDING,
alice,
0,
1,
maxIn,
0,
0,
false
);
vm.stopPrank();
// Pre-funded amounts should match reference swap amounts
assertEq(preAmountIn, refAmountIn, "Pre-funded amountIn should match reference");
assertEq(preAmountOut, refAmountOut, "Pre-funded amountOut should match reference");
assertEq(preFee, refFee, "Pre-funded fee should match reference");
}
/// @notice Verify that callback swap amounts match swapAmounts() view predictions
function testCallbackMatchesSwapAmountsView() public {
uint256 maxIn = 10_000;
// Perform a reference swap
vm.startPrank(bob);
token0.approve(address(pool), type(uint256).max);
(uint256 refAmountIn, uint256 refAmountOut, uint256 refFee) = pool.swap(
bob,
Funding.USE_APPROVALS,
bob,
0,
1,
maxIn,
0,
0,
false
);
vm.stopPrank();
// Create fresh pool for callback test
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(token2));
uint256 feePpm = 1000;
int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool testPool = Deploy.newPartyPool(address(this), "LP_TEST2", "LP_TEST2", tokens, kappa, feePpm, feePpm, false);
token0.mint(address(this), INIT_BAL);
token1.mint(address(this), INIT_BAL);
token2.mint(address(this), INIT_BAL);
token0.transfer(address(testPool), INIT_BAL);
token1.transfer(address(testPool), INIT_BAL);
token2.transfer(address(testPool), INIT_BAL);
testPool.initialMint(address(this), INIT_BAL * tokens.length * 10**18);
// Setup callback for test pool
SwapCallbackContract testCallback = new SwapCallbackContract(address(testPool));
testCallback.setTokenSource(alice);
testCallback.setShouldFail(false);
vm.startPrank(alice);
token0.approve(address(testCallback), type(uint256).max);
vm.stopPrank();
// Test callback with same initial state
(uint256 cbAmountIn, uint256 cbAmountOut, uint256 cbFee) = testPool.swap(
address(testCallback),
CALLBACK,
alice,
0,
1,
maxIn,
0,
0,
false
);
// Callback amounts should match reference swap amounts
assertEq(cbAmountIn, refAmountIn, "Callback amountIn should match reference");
assertEq(cbAmountOut, refAmountOut, "Callback amountOut should match reference");
assertEq(cbFee, refFee, "Callback fee should match reference");
}
/// @notice Test multiple swaps in sequence with different funding methods produce consistent results
function testMultipleSwapsFundingMethodsConsistency() public {
uint256[] memory swapAmounts = new uint256[](3);
swapAmounts[0] = 5_000;
swapAmounts[1] = 7_500;
swapAmounts[2] = 10_000;
for (uint i = 0; i < swapAmounts.length; i++) {
uint256 swapAmount = swapAmounts[i];
// Create three identical pools
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(token2));
int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool poolApproval = Deploy.newPartyPool(address(this), "LP_A", "LP_A", tokens, kappa, 0, 0, false);
PartyPool poolPreFund = Deploy.newPartyPool(address(this), "LP_P", "LP_P", tokens, kappa, 0, 0, false);
PartyPool poolCallback = Deploy.newPartyPool(address(this), "LP_C", "LP_C", tokens, kappa, 0, 0, false);
// Initialize all three pools identically
token0.mint(address(this), INIT_BAL * 3);
token1.mint(address(this), INIT_BAL * 3);
token2.mint(address(this), INIT_BAL * 3);
token0.transfer(address(poolApproval), INIT_BAL);
token1.transfer(address(poolApproval), INIT_BAL);
token2.transfer(address(poolApproval), INIT_BAL);
poolApproval.initialMint(address(this), 0);
token0.transfer(address(poolPreFund), INIT_BAL);
token1.transfer(address(poolPreFund), INIT_BAL);
token2.transfer(address(poolPreFund), INIT_BAL);
poolPreFund.initialMint(address(this), 0);
token0.transfer(address(poolCallback), INIT_BAL);
token1.transfer(address(poolCallback), INIT_BAL);
token2.transfer(address(poolCallback), INIT_BAL);
poolCallback.initialMint(address(this), 0);
// Test with USE_APPROVALS (bytes4(0) with approvals)
vm.startPrank(alice);
token0.approve(address(poolApproval), type(uint256).max);
(uint256 apprIn, uint256 apprOut, ) = poolApproval.swap(
alice, Funding.USE_APPROVALS, alice, 0, 1, swapAmount, 0, 0, false
);
vm.stopPrank();
// Test with PRE_FUNDED (bytes4(0) with pre-funding)
vm.startPrank(alice);
token0.transfer(address(poolPreFund), swapAmount);
(uint256 preIn, uint256 preOut, ) = poolPreFund.swap(
alice, Funding.USE_PREFUNDING, alice, 0, 1, swapAmount, 0, 0, false
);
vm.stopPrank();
// Test with CALLBACK
SwapCallbackContract cb = new SwapCallbackContract(address(poolCallback));
cb.setTokenSource(alice);
cb.setShouldFail(false);
vm.startPrank(alice);
token0.approve(address(cb), type(uint256).max);
vm.stopPrank();
(uint256 cbIn, uint256 cbOut, ) = poolCallback.swap(
address(cb), CALLBACK, alice, 0, 1, swapAmount, 0, 0, false
);
// All three methods should produce identical results
assertEq(preIn, apprIn, "Pre-funded input should match approval");
assertEq(preOut, apprOut, "Pre-funded output should match approval");
assertEq(cbIn, apprIn, "Callback input should match approval");
assertEq(cbOut, apprOut, "Callback output should match approval");
}
}
}
/* solhint-enable */

View File

@@ -12,10 +12,10 @@ import {IERC3156FlashBorrower} from "../lib/openzeppelin-contracts/contracts/int
import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {Funding} from "../src/Funding.sol";
import {IPartyPool} from "../src/IPartyPool.sol";
import {LMSRStabilized} from "../src/LMSRStabilized.sol";
import {PartyPlanner} from "../src/PartyPlanner.sol";
import {PartyPool} from "../src/PartyPool.sol";
import {Deploy} from "./Deploy.sol";
import {TestERC20, FlashBorrower} from "./GasTest.sol";
@@ -238,7 +238,7 @@ contract GasTest is Test {
/// @notice Helper function: perform 10 swaps back-and-forth between the first two _tokens.
function _performSwapGasTest(IPartyPool testPool) internal {
_performSwapGasTest(testPool, false);
_performSwapGasTest(testPool, Funding.USE_APPROVALS);
}
function sendTokensCallback(IERC20 token, uint256 amount) external {
@@ -247,44 +247,52 @@ contract GasTest is Test {
token.transferFrom( alice, msg.sender, amount);
}
function _performSwapGasTest(IPartyPool testPool, bool useCallback) internal {
function _performSwapGasTest(IPartyPool testPool, bytes4 fundingSelector) internal {
IERC20[] memory tokens = testPool.allTokens();
require(tokens.length >= 2, "Pool must have at least 2 tokens");
address payer;
address spender;
bytes4 selector;
if (useCallback) {
if (fundingSelector == Funding.USE_PREFUNDING) {
payer = address(this);
spender = address(this);
selector = this.sendTokensCallback.selector;
}
else {
else if (fundingSelector == Funding.USE_APPROVALS) {
payer = alice;
spender = address(testPool);
selector = bytes4(0);
}
else {
payer = address(this);
spender = address(this);
}
TestERC20 token0 = TestERC20(address(tokens[0]));
TestERC20 token1 = TestERC20(address(tokens[1]));
vm.prank(alice);
TestERC20(address(tokens[0])).approve(spender, type(uint256).max);
token0.approve(spender, type(uint256).max);
vm.prank(alice);
TestERC20(address(tokens[1])).approve(spender, type(uint256).max);
token1.approve(spender, type(uint256).max);
uint256 maxIn = 10_000;
// Perform swaps alternating directions to avoid large imbalance
vm.startPrank(alice);
for (uint256 i = 0; i < 20; i++) {
vm.prank(alice);
if (i % 2 == 0) {
if (fundingSelector == Funding.USE_PREFUNDING)
token0.transfer(address(testPool), maxIn);
// swap token0 -> token1
testPool.swap(payer, selector, alice, 0, 1, maxIn, 0, 0, false);
testPool.swap(payer, fundingSelector, alice, 0, 1, maxIn, 0, 0, false);
} else {
// swap token1 -> token0
testPool.swap(payer, selector, alice, 1, 0, maxIn, 0, 0, false);
if (fundingSelector == Funding.USE_PREFUNDING)
token1.transfer(address(testPool), maxIn);
testPool.swap(payer, fundingSelector, alice, 1, 0, maxIn, 0, 0, false);
}
// shake up the bits
maxIn *= 787;
maxIn /= 1000;
}
vm.stopPrank();
}
/// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 2-token pool.
@@ -299,9 +307,13 @@ contract GasTest is Test {
/// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 10-token pool using the callback funding method.
function testSwapGasCallback() public {
_performSwapGasTest(pool10, true);
_performSwapGasTest(pool10, this.sendTokensCallback.selector);
}
/// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 10-token pool using the callback funding method.
function testSwapGasPrefunding() public {
_performSwapGasTest(pool10, Funding.USE_PREFUNDING);
}
/// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 20-token pool.
function testSwapGasTwenty() public {

View File

@@ -2,15 +2,21 @@
/* solhint-disable */
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "@abdk/ABDKMath64x64.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../src/LMSRStabilized.sol";
import "../src/PartyPool.sol";
import {NativeWrapper} from "../src/NativeWrapper.sol";
import {PartyPlanner} from "../src/PartyPlanner.sol";
import {Deploy} from "./Deploy.sol";
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import {CommonBase} from "../lib/forge-std/src/Base.sol";
import {StdAssertions} from "../lib/forge-std/src/StdAssertions.sol";
import {StdChains} from "../lib/forge-std/src/StdChains.sol";
import {StdCheats, StdCheatsSafe} from "../lib/forge-std/src/StdCheats.sol";
import {StdUtils} from "../lib/forge-std/src/StdUtils.sol";
import {Test} from "../lib/forge-std/src/Test.sol";
import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {Funding} from "../src/Funding.sol";
import {LMSRStabilized} from "../src/LMSRStabilized.sol";
import {PartyInfo} from "../src/PartyInfo.sol";
import {PartyPool} from "../src/PartyPool.sol";
import {Deploy} from "./Deploy.sol";
import {TestERC20Native} from "./NativeTest.t.sol";
import {WETH9} from "./WETH9.sol";
/// @notice Minimal ERC20 token for tests with an external mint function.
@@ -142,7 +148,7 @@ contract NativeTest is Test {
// Send native currency with {value: maxIn}
(uint256 amountIn, uint256 amountOut, ) = pool.swap{value: maxIn}(
alice, // payer
bytes4(0),
Funding.USE_APPROVALS,
alice, // receiver
2, // inputTokenIndex (WETH)
0, // outputTokenIndex (token0)
@@ -180,7 +186,7 @@ contract NativeTest is Test {
// Execute swap: token0 (index 0) -> WETH (index 2) with unwrap=true
(uint256 amountIn, uint256 amountOut, ) = pool.swap(
alice, // payer
bytes4(0), // no selector: use ERC20 approvals
Funding.USE_APPROVALS, // no selector: use ERC20 approvals
alice, // receiver
0, // inputTokenIndex (token0)
2, // outputTokenIndex (WETH)
@@ -216,7 +222,7 @@ contract NativeTest is Test {
// Execute swap with excess native currency
(uint256 amountIn, , ) = pool.swap{value: totalSent}(
alice, // payer
bytes4(0),
Funding.USE_APPROVALS,
alice, // receiver
2, // inputTokenIndex (WETH)
0, // outputTokenIndex (token0)
@@ -545,14 +551,14 @@ contract NativeTest is Test {
// 2. Swap native currency for token0
uint256 swapAmount = 5_000;
(, uint256 amountOut, ) = pool.swap{value: swapAmount}(
alice,bytes4(0),alice, 2, 0, swapAmount, 0, 0, false
alice,Funding.USE_APPROVALS,alice, 2, 0, swapAmount, 0, 0, false
);
assertTrue(amountOut > 0, "Should receive token0");
// 3. Swap token0 back to native currency
uint256 token0Balance = token0.balanceOf(alice);
(, uint256 swapOut2, ) = pool.swap(
alice, bytes4(0), alice, 0, 2, token0Balance / 2, 0, 0, true
alice, Funding.USE_APPROVALS, alice, 0, 2, token0Balance / 2, 0, 0, true
);
assertTrue(swapOut2 > 0, "Should receive native currency");
@@ -579,7 +585,7 @@ contract NativeTest is Test {
// Swap token0 -> WETH without unwrap
(, uint256 amountOut, ) = pool.swap(
alice, bytes4(0), alice, 0, 2, maxIn, 0, 0, false // unwrap=false
alice, Funding.USE_APPROVALS, alice, 0, 2, maxIn, 0, 0, false // unwrap=false
);
assertTrue(amountOut > 0, "Should receive WETH tokens");
@@ -600,7 +606,7 @@ contract NativeTest is Test {
// Try to swap token0 (not WETH) by sending native currency - should revert
vm.expectRevert();
pool.swap{value: 10_000}(
alice, bytes4(0), alice, 0, 1, 10_000, 0, 0, false
alice, Funding.USE_APPROVALS, alice, 0, 1, 10_000, 0, 0, false
);
vm.stopPrank();

View File

@@ -2,17 +2,25 @@
/* solhint-disable */
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "@abdk/ABDKMath64x64.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../src/LMSRStabilized.sol";
import "../src/PartyPool.sol";
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import {CommonBase} from "../lib/forge-std/src/Base.sol";
import {StdAssertions} from "../lib/forge-std/src/StdAssertions.sol";
import {StdChains} from "../lib/forge-std/src/StdChains.sol";
import {StdCheats, StdCheatsSafe} from "../lib/forge-std/src/StdCheats.sol";
import {StdUtils} from "../lib/forge-std/src/StdUtils.sol";
import {Test} from "../lib/forge-std/src/Test.sol";
import {IERC3156FlashBorrower} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {Funding} from "../src/Funding.sol";
import {LMSRStabilized} from "../src/LMSRStabilized.sol";
import {PartyInfo} from "../src/PartyInfo.sol";
import {PartyPlanner} from "../src/PartyPlanner.sol";
import {PartyPool} from "../src/PartyPool.sol";
import {Deploy} from "./Deploy.sol";
import {TestERC20, FlashBorrower} from "./PartyPool.t.sol";
// Import the flash callback interface
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
import {PartyPlanner} from "../src/PartyPlanner.sol";
import {Deploy} from "./Deploy.sol";
import {PartyInfo} from "../src/PartyInfo.sol";
/// @notice Test contract that implements the flash callback for testing flash loans
contract FlashBorrower is IERC3156FlashBorrower {
@@ -423,7 +431,7 @@ contract PartyPoolTest is Test {
// Execute swap: token0 -> token1
vm.prank(alice);
(uint256 amountInUsed, uint256 amountOut, uint256 fee) = pool.swap(alice, bytes4(0), bob, 0, 1, maxIn, 0, 0, false);
(uint256 amountInUsed, uint256 amountOut, uint256 fee) = pool.swap(alice, Funding.USE_APPROVALS, bob, 0, 1, maxIn, 0, 0, false);
// Amounts should be positive and not exceed provided max
assertTrue(amountInUsed > 0, "expected some input used");
@@ -452,7 +460,7 @@ contract PartyPoolTest is Test {
vm.prank(alice);
vm.expectRevert(bytes("LMSR: limitPrice <= current price"));
pool.swap(alice, bytes4(0), alice, 0, 1, 1000, limitPrice, 0, false);
pool.swap(alice, Funding.USE_APPROVALS, alice, 0, 1, 1000, limitPrice, 0, false);
}
/// @notice swapToLimit should compute input needed to reach a slightly higher price and execute.
@@ -1024,8 +1032,8 @@ contract PartyPoolTest is Test {
token0.approve(address(poolCustom), type(uint256).max);
// Perform identical swaps: token0 -> token1
(uint256 amountInDefault, uint256 amountOutDefault, uint256 feeDefault) = poolDefault.swap(alice, bytes4(0), alice, 0, 1, swapAmount, 0, 0, false);
(uint256 amountInCustom, uint256 amountOutCustom, uint256 feeCustom) = poolCustom.swap(alice, bytes4(0), alice, 0, 1, swapAmount, 0, 0, false);
(uint256 amountInDefault, uint256 amountOutDefault, uint256 feeDefault) = poolDefault.swap(alice, Funding.USE_APPROVALS, alice, 0, 1, swapAmount, 0, 0, false);
(uint256 amountInCustom, uint256 amountOutCustom, uint256 feeCustom) = poolCustom.swap(alice, Funding.USE_APPROVALS, alice, 0, 1, swapAmount, 0, 0, false);
// Swap results should be identical
assertEq(amountInDefault, amountInCustom, "Swap input amounts should be identical");