prefunding
This commit is contained in:
@@ -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 "%s\n" " ------ --------- ---------"
|
||||
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
11
src/Funding.sol
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
610
test/FundingSwapTest.sol
Normal 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 */
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user