Files
lmsr-amm/test/NativeTest.t.sol
2025-11-11 00:21:18 -04:00

613 lines
22 KiB
Solidity

// SPDX-License-Identifier: UNLICENSED
/* 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 {PartyInfo} from "../src/PartyInfo.sol";
import {WETH9} from "./WETH9.sol";
/// @notice Minimal ERC20 token for tests with an external mint function.
contract TestERC20Native 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);
}
// Expose convenient approve helper for tests
function approveMax(address spender) external {
_approve(msg.sender, spender, type(uint256).max);
}
}
/// @notice Tests for PartyPool native currency (ETH) functionality with WETH wrapping/unwrapping.
/// @dev This test contract creates a pool where one of the assets is WETH, then tests all operations
/// that can send or receive native currency by using unwrap=true and {value:amount} syntax.
contract NativeTest is Test {
using ABDKMath64x64 for int128;
TestERC20Native token0;
TestERC20Native token1;
WETH9 weth; // WETH is our third token
PartyPool pool;
PartyInfo info;
address alice;
address bob;
// Common parameters
int128 tradeFrac;
int128 targetSlippage;
uint256 constant INIT_BAL = 1_000_000; // initial token units for each token
uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers
function setUp() public {
alice = address(0xA11ce);
bob = address(0xB0b);
// Give alice and bob native currency for testing
vm.deal(alice, 100 ether);
vm.deal(bob, 100 ether);
// Deploy two regular ERC20 _tokens
token0 = new TestERC20Native("T0", "T0", 0);
token1 = new TestERC20Native("T1", "T1", 0);
// Deploy WETH
weth = new WETH9();
// Mint initial balances to this test contract
token0.mint(address(this), INIT_BAL);
token1.mint(address(this), INIT_BAL);
// For WETH, we deposit native currency to get wrapped _tokens
weth.deposit{value: 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: [token0, token1, WETH]
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(weth)); // WETH as third token
uint256[] memory bases = new uint256[](3);
bases[0] = BASE;
bases[1] = BASE;
bases[2] = BASE;
// 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, weth, false);
// Transfer initial deposit amounts into pool
token0.transfer(address(pool), INIT_BAL);
token1.transfer(address(pool), INIT_BAL);
weth.transfer(address(pool), INIT_BAL);
// Perform initial mint
pool.initialMint(address(this), 0);
// Mint _tokens to alice and bob for testing
token0.mint(alice, INIT_BAL);
token1.mint(alice, INIT_BAL);
token0.mint(bob, INIT_BAL);
token1.mint(bob, INIT_BAL);
info = Deploy.newInfo();
}
/// @notice Helper to verify refunds work correctly
modifier expectRefund(address user, uint256 sent, uint256 expectedUsed) {
uint256 balBefore = user.balance;
_;
uint256 balAfter = user.balance;
uint256 refund = sent - expectedUsed;
assertEq(balAfter, balBefore - expectedUsed, "User should be refunded unused native currency");
}
/* ----------------------
Swap Tests with Native Currency
---------------------- */
/// @notice Test swap with native currency as input (token index 2 = WETH)
/// @dev Send ETH to pool, which should wrap it as WETH and execute the swap
function testSwapWithNativeInput() public {
uint256 maxIn = 10_000;
// Alice swaps native currency (ETH -> WETH input) for token0 output
vm.startPrank(alice);
uint256 aliceEthBefore = alice.balance;
uint256 aliceToken0Before = token0.balanceOf(alice);
// Execute swap: WETH (index 2) -> token0 (index 0)
// Send native currency with {value: maxIn}
(uint256 amountIn, uint256 amountOut, ) = pool.swap{value: maxIn}(
alice, // payer
bytes4(0),
alice, // receiver
2, // inputTokenIndex (WETH)
0, // outputTokenIndex (token0)
maxIn, // maxAmountIn
0, // limitPrice
0, // deadline
false // unwrap (output is not WETH, so false)
);
// 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");
// Alice's ETH balance should decrease by amountIn
assertEq(alice.balance, aliceEthBefore - amountIn, "Alice ETH should decrease by amountIn");
// Alice's token0 balance should increase by amountOut
assertEq(token0.balanceOf(alice), aliceToken0Before + amountOut, "Alice token0 should increase");
vm.stopPrank();
}
/// @notice Test swap with native currency as output (unwrap=true)
/// @dev Swap token0 for WETH, then unwrap WETH to native currency
function testSwapWithNativeOutput() public {
uint256 maxIn = 10_000;
vm.startPrank(alice);
token0.approve(address(pool), type(uint256).max);
uint256 aliceToken0Before = token0.balanceOf(alice);
uint256 aliceEthBefore = alice.balance;
// 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
alice, // receiver
0, // inputTokenIndex (token0)
2, // outputTokenIndex (WETH)
maxIn, // maxAmountIn
0, // limitPrice
0, // deadline
true // unwrap (receive native currency instead of WETH)
);
// Verify amounts
assertTrue(amountIn > 0, "expected some input used");
assertTrue(amountOut > 0, "expected some output returned");
// Alice's token0 balance should decrease by amountIn
assertEq(token0.balanceOf(alice), aliceToken0Before - amountIn, "Alice token0 should decrease");
// Alice's ETH balance should increase by amountOut (unwrapped)
assertEq(alice.balance, aliceEthBefore + amountOut, "Alice ETH should increase by unwrapped amount");
vm.stopPrank();
}
/// @notice Test swap with excess native currency sent - verify refund
function testSwapWithExcessNativeRefunded() public {
uint256 maxIn = 10_000;
uint256 excessAmount = 5_000;
uint256 totalSent = maxIn + excessAmount;
vm.startPrank(alice);
uint256 aliceEthBefore = alice.balance;
// Execute swap with excess native currency
(uint256 amountIn, , ) = pool.swap{value: totalSent}(
alice, // payer
bytes4(0),
alice, // receiver
2, // inputTokenIndex (WETH)
0, // outputTokenIndex (token0)
maxIn, // maxAmountIn
0, // limitPrice
0, // deadline
false // unwrap
);
// Verify that only amountIn was used, and excess was refunded
assertTrue(amountIn <= maxIn, "used input must not exceed max");
assertEq(alice.balance, aliceEthBefore - amountIn, "Alice should be refunded excess ETH");
vm.stopPrank();
}
/// @notice Test swapToLimit with native currency input
function testSwapToLimitWithNativeInput() public {
// Choose a limit price slightly above current (~1)
int128 limitPrice = ABDKMath64x64.fromInt(1).add(ABDKMath64x64.divu(1, 1000));
vm.startPrank(alice);
uint256 aliceEthBefore = alice.balance;
// Execute swapToLimit: WETH (index 2) -> token0 (index 0)
// Send a large amount of native currency; pool will only use what's needed
uint256 largeAmount = 100_000;
(uint256 amountInUsed, uint256 amountOut, uint256 fee) = pool.swapToLimit{value: largeAmount}(
alice, // payer
alice, // receiver
2, // inputTokenIndex (WETH)
0, // outputTokenIndex (token0)
limitPrice, // limitPrice
0, // deadline
false // unwrap
);
assertTrue(amountInUsed > 0, "expected some input used for swapToLimit");
assertTrue(amountOut > 0, "expected some output for swapToLimit");
// Alice should be refunded unused ETH
assertTrue(alice.balance > aliceEthBefore - largeAmount, "Alice should be refunded");
assertEq(alice.balance, aliceEthBefore - amountInUsed - fee, "Alice ETH balance check");
vm.stopPrank();
}
/// @notice Test swapToLimit with native currency output (unwrap=true)
function testSwapToLimitWithNativeOutput() public {
int128 limitPrice = ABDKMath64x64.fromInt(1).add(ABDKMath64x64.divu(1, 1000));
vm.startPrank(alice);
token0.approve(address(pool), type(uint256).max);
uint256 aliceEthBefore = alice.balance;
// Execute swapToLimit: token0 (index 0) -> WETH (index 2) with unwrap=true
(uint256 amountInUsed, uint256 amountOut, /*uint256 fee*/) = pool.swapToLimit(
alice, // payer
alice, // receiver
0, // inputTokenIndex (token0)
2, // outputTokenIndex (WETH)
limitPrice, // limitPrice
0, // deadline
true // unwrap (receive native currency)
);
assertTrue(amountInUsed > 0, "expected some input used");
assertTrue(amountOut > 0, "expected some output");
// Alice should receive native currency
assertTrue(alice.balance > aliceEthBefore, "Alice should receive ETH");
vm.stopPrank();
}
/* ----------------------
Mint Tests with Native Currency
---------------------- */
/// @notice Test proportional mint with native currency input
function testMintWithNativeInput() public {
uint256 lpRequest = pool.totalSupply() / 10; // Request 10% of pool
// Get required deposit amounts
uint256[] memory deposits = info.mintAmounts(pool, lpRequest);
vm.startPrank(alice);
token0.approve(address(pool), type(uint256).max);
token1.approve(address(pool), type(uint256).max);
// For WETH, we send native currency instead of approving
uint256 aliceEthBefore = alice.balance;
uint256 wethDeposit = deposits[2]; // WETH is index 2
// Perform mint with native currency for WETH portion
uint256 lpMinted = pool.mint{value: wethDeposit}(
alice, // payer
alice, // receiver
lpRequest, // lpTokenAmount
0 // deadline
);
assertTrue(lpMinted > 0, "LP should be minted");
// Alice's ETH should decrease by WETH deposit amount
assertEq(alice.balance, aliceEthBefore - wethDeposit, "Alice ETH should decrease");
vm.stopPrank();
}
/// @notice Test mint with excess native currency - verify refund
function testMintWithExcessNativeRefunded() public {
uint256 lpRequest = pool.totalSupply() / 10;
uint256[] memory deposits = info.mintAmounts(pool, lpRequest);
vm.startPrank(alice);
token0.approve(address(pool), type(uint256).max);
token1.approve(address(pool), type(uint256).max);
uint256 aliceEthBefore = alice.balance;
uint256 wethDeposit = deposits[2];
uint256 excess = 10_000;
uint256 totalSent = wethDeposit + excess;
// Send excess native currency
uint256 lpMinted = pool.mint{value: totalSent}(
alice,
alice,
lpRequest,
0
);
assertTrue(lpMinted > 0, "LP should be minted");
// Alice should be refunded the excess
assertEq(alice.balance, aliceEthBefore - wethDeposit, "Alice should be refunded excess");
vm.stopPrank();
}
/* ----------------------
Burn Tests with Native Currency
---------------------- */
/// @notice Test burn with native currency output (unwrap=true)
function testBurnWithNativeOutput() public {
uint256 lpToBurn = pool.totalSupply() / 10;
// Get expected withdraw amounts
uint256[] memory withdraws = info.burnAmounts(pool, lpToBurn);
uint256 thisEthBefore = address(this).balance;
uint256 expectedWethWithdraw = withdraws[2]; // WETH is index 2
// Burn LP with unwrap=true to receive native currency for WETH portion
uint256[] memory actualWithdraws = pool.burn(
address(this), // payer (this contract holds LP from setUp)
address(this), // receiver
lpToBurn, // lpAmount
0, // deadline
true // unwrap (receive native currency for WETH)
);
// Verify we received the expected amounts
assertEq(actualWithdraws[0], withdraws[0], "token0 withdraw amount");
assertEq(actualWithdraws[1], withdraws[1], "token1 withdraw amount");
assertEq(actualWithdraws[2], withdraws[2], "WETH withdraw amount");
// Verify we received native currency for WETH portion
assertEq(address(this).balance, thisEthBefore + expectedWethWithdraw, "Should receive ETH for WETH");
}
/// @notice Test burn to a different receiver with native output
function testBurnToReceiverWithNativeOutput() public {
uint256 lpToBurn = pool.totalSupply() / 10;
uint256[] memory withdraws = info.burnAmounts(pool, lpToBurn);
uint256 bobEthBefore = bob.balance;
uint256 bobToken0Before = token0.balanceOf(bob);
uint256 bobToken1Before = token1.balanceOf(bob);
// Burn LP and send to bob with unwrap=true
pool.burn(
address(this), // payer
bob, // receiver
lpToBurn,
0,
true // unwrap
);
// Bob should receive _tokens and native currency
assertEq(token0.balanceOf(bob), bobToken0Before + withdraws[0], "Bob token0");
assertEq(token1.balanceOf(bob), bobToken1Before + withdraws[1], "Bob token1");
assertEq(bob.balance, bobEthBefore + withdraws[2], "Bob should receive ETH");
}
/* ----------------------
SwapMint Tests with Native Currency
---------------------- */
/// @notice Test swapMint with native currency input
function testSwapMintWithNativeInput() public {
uint256 maxIn = 10_000;
vm.startPrank(alice);
uint256 aliceEthBefore = alice.balance;
uint256 aliceLpBefore = pool.balanceOf(alice);
// Call swapMint with native currency: deposit ETH as WETH (index 2)
(, uint256 lpMinted,) = pool.swapMint{value: maxIn}(
alice, // payer
alice, // receiver
2, // inputTokenIndex (WETH)
maxIn, // maxAmountIn
0 // deadline
);
assertTrue(lpMinted > 0, "swapMint should mint LP");
// Alice's ETH should decrease (by at most maxIn)
assertTrue(alice.balance <= aliceEthBefore, "Alice ETH should decrease");
assertTrue(aliceEthBefore - alice.balance <= maxIn, "Alice spent at most maxIn");
// Alice should receive LP _tokens
assertTrue(pool.balanceOf(alice) >= aliceLpBefore + lpMinted, "Alice should receive LP");
vm.stopPrank();
}
/// @notice Test swapMint with excess native currency - verify refund
function testSwapMintWithExcessNativeRefunded() public {
uint256 maxIn = 10_000;
uint256 excess = 20_000;
uint256 totalSent = maxIn + excess;
vm.startPrank(alice);
uint256 aliceEthBefore = alice.balance;
// Send excess native currency
(, uint256 lpMinted,) = pool.swapMint{value: totalSent}(
alice,
alice,
2, // WETH
maxIn,
0
);
assertTrue(lpMinted > 0, "swapMint should mint LP");
// Alice should not lose more than maxIn
assertTrue(aliceEthBefore - alice.balance <= maxIn, "Alice should be refunded excess");
vm.stopPrank();
}
/* ----------------------
BurnSwap Tests with Native Currency
---------------------- */
/// @notice Test burnSwap with native currency output (unwrap=true)
function testBurnSwapWithNativeOutput() public {
uint256 lpToBurn = pool.totalSupply() / 10;
uint256 thisEthBefore = address(this).balance;
// Burn LP and receive all proceeds as native currency (WETH unwrapped)
(uint256 payout, ) = pool.burnSwap(
address(this), // payer (holds LP)
address(this), // receiver
lpToBurn, // lpAmount
2, // inputTokenIndex (WETH)
0, // deadline
true // unwrap (receive native currency)
);
assertTrue(payout > 0, "burnSwap should produce payout");
// This contract should receive native currency
assertEq(address(this).balance, thisEthBefore + payout, "Should receive ETH");
}
/// @notice Test burnSwap to different receiver with native output
function testBurnSwapToReceiverWithNativeOutput() public {
uint256 lpToBurn = pool.totalSupply() / 10;
uint256 bobEthBefore = bob.balance;
// Burn LP and send native currency to bob
(uint256 payout, ) = pool.burnSwap(
address(this), // payer
bob, // receiver
lpToBurn,
2, // WETH
0,
true // unwrap
);
assertTrue(payout > 0, "burnSwap should produce payout");
// Bob should receive native currency
assertEq(bob.balance, bobEthBefore + payout, "Bob should receive ETH");
}
/* ----------------------
Combined Native Operations
---------------------- */
/// @notice Test full cycle: mint with native -> swap with native -> burn with native
function testFullCycleWithNative() public {
vm.startPrank(alice);
// 1. Mint with native currency
uint256 lpRequest = pool.totalSupply() / 20; // 5% of pool
uint256[] memory deposits = info.mintAmounts(pool, lpRequest);
token0.approve(address(pool), type(uint256).max);
token1.approve(address(pool), type(uint256).max);
uint256 lpMinted = pool.mint{value: deposits[2]}(alice, alice, lpRequest, 0);
assertTrue(lpMinted > 0, "Should mint LP");
// 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
);
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
);
assertTrue(swapOut2 > 0, "Should receive native currency");
// 4. Burn LP to native currency
uint256 lpToBurn = lpMinted / 2;
(uint256 payout, ) = pool.burnSwap(alice, alice, lpToBurn, 2, 0, true);
assertTrue(payout > 0, "Should receive payout in native");
// Alice should have some ETH back (maybe more or less depending on slippage)
assertTrue(alice.balance > 0, "Alice should have some ETH");
vm.stopPrank();
}
/// @notice Test that unwrap=false with WETH actually transfers WETH _tokens (not native)
function testSwapWithWethNoUnwrap() public {
uint256 maxIn = 10_000;
vm.startPrank(alice);
token0.approve(address(pool), type(uint256).max);
uint256 aliceWethBefore = weth.balanceOf(alice);
uint256 aliceEthBefore = alice.balance;
// Swap token0 -> WETH without unwrap
(, uint256 amountOut, ) = pool.swap(
alice, bytes4(0), alice, 0, 2, maxIn, 0, 0, false // unwrap=false
);
assertTrue(amountOut > 0, "Should receive WETH tokens");
// Alice's WETH balance should increase
assertEq(weth.balanceOf(alice), aliceWethBefore + amountOut, "Alice should receive WETH tokens");
// Alice's ETH balance should not change (except for gas, but we don't track that)
assertEq(alice.balance, aliceEthBefore, "Alice ETH should not change with unwrap=false");
vm.stopPrank();
}
/// @notice Verify that sending native currency for non-WETH input reverts
function testSwapNativeForNonWethReverts() public {
vm.startPrank(alice);
// 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
);
vm.stopPrank();
}
// Make this contract payable to receive native currency from pool
receive() external payable {}
}
/* solhint-enable */